# WebhookRelay ## Table of Contents - [Webhook Relay - forward webhooks to any destination](#webhook-relay---forward-webhooks-to-any-destination) - [Webhook Guides & Resources | WebhookRelay](#webhook-guides---resources---webhookrelay) - [Localhost Tunnels — Expose Your Local Server | WebhookRelay](#localhost-tunnels---expose-your-local-server---webhookrelay) - [The State of Webhooks — live webhook traffic data](#the-state-of-webhooks---live-webhook-traffic-data) - [Datadog](#datadog) - [Quora](#quora) - [Webhook Gateway — Durable Webhook Delivery | WebhookRelay](#webhook-gateway---durable-webhook-delivery---webhookrelay) - [Custom Subdomains | WebhookRelay](#custom-subdomains---webhookrelay) - [Alerts | WebhookRelay](#alerts---webhookrelay) - [Static Outgoing IP Address | WebhookRelay](#static-outgoing-ip-address---webhookrelay) - [Forward Webhooks to Multiple Destinations | WebhookRelay](#forward-webhooks-to-multiple-destinations---webhookrelay) - [Getting Started | WebhookRelay](#getting-started---webhookrelay) - [Forwarding Rules - Filter and Route Webhooks | WebhookRelay](#forwarding-rules---filter-and-route-webhooks---webhookrelay) - [Webhook Logs & Delivery Monitoring | WebhookRelay](#webhook-logs---delivery-monitoring---webhookrelay) - [Durable Webhook Retries | WebhookRelay](#durable-webhook-retries---webhookrelay) - [Webhooks to Internal Servers | WebhookRelay](#webhooks-to-internal-servers---webhookrelay) - [Tutorials | WebhookRelay](#tutorials---webhookrelay) - [Webhook Throttling | WebhookRelay](#webhook-throttling---webhookrelay) - [Custom Domains | WebhookRelay](#custom-domains---webhookrelay) - [Serverless Webhook Transformations | WebhookRelay](#serverless-webhook-transformations---webhookrelay) - [Single Sign-On (SSO) | WebhookRelay](#single-sign-on--sso----webhookrelay) - [Transform Webhooks with AI | WebhookRelay](#transform-webhooks-with-ai---webhookrelay) - [Teams | WebhookRelay](#teams---webhookrelay) - [Free CloudEvents Validator — Validate JSON | WebhookRelay](#free-cloudevents-validator---validate-json---webhookrelay) - [Schedule recurring webhooks | WebhookRelay](#schedule-recurring-webhooks---webhookrelay) - [Webhook Relay Kubernetes Integration | WebhookRelay](#webhook-relay-kubernetes-integration---webhookrelay) - [Rewriting Host Header | WebhookRelay](#rewriting-host-header---webhookrelay) - [Audit Logs | WebhookRelay](#audit-logs---webhookrelay) - [Custom Roles | WebhookRelay](#custom-roles---webhookrelay) - [Free Webhook Signature Verifier Online | WebhookRelay](#free-webhook-signature-verifier-online---webhookrelay) - [Receive emails as webhooks | WebhookRelay](#receive-emails-as-webhooks---webhookrelay) - [Webhook Relay - FAQ & Contact | WebhookRelay](#webhook-relay---faq---contact---webhookrelay) - [Forward Webhooks to Localhost & Internal Servers](#forward-webhooks-to-localhost---internal-servers) - [Email to Webhook — Inbound Email API | WebhookRelay](#email-to-webhook---inbound-email-api---webhookrelay) - [Webhook to Slack, Discord & Teams Formatter | WebhookRelay](#webhook-to-slack--discord---teams-formatter---webhookrelay) - [Free URL Redirect Generator — 301 & 302 | WebhookRelay](#free-url-redirect-generator---301---302---webhookrelay) - [How to receive Stripe webhooks on localhost | WebhookRelay](#how-to-receive-stripe-webhooks-on-localhost---webhookrelay) - [What Is a Webhook? Definition, Examples & How They Work](#what-is-a-webhook--definition--examples---how-they-work) - [Receive GitHub Webhooks Locally | WebhookRelay](#receive-github-webhooks-locally---webhookrelay) - [How to Verify a Webhook Signature (HMAC SHA256)](#how-to-verify-a-webhook-signature--hmac-sha256-) - [Webhook vs API: What's the Difference? (With Examples)](#webhook-vs-api--what-s-the-difference---with-examples-) - [How to Test Webhooks: The Complete Guide (2026)](#how-to-test-webhooks--the-complete-guide--2026-) - [ngrok Alternative: Persistent Webhook URLs | WebhookRelay](#ngrok-alternative--persistent-webhook-urls---webhookrelay) - [Terms of Service | WebhookRelay](#terms-of-service---webhookrelay) - [Guide to the General Data Protection (GDPR) | WebhookRelay](#guide-to-the-general-data-protection--gdpr----webhookrelay) - [Webhook Security: Best Practices to Secure Your Webhooks](#webhook-security--best-practices-to-secure-your-webhooks) - [About Webhook Relay | WebhookRelay](#about-webhook-relay---webhookrelay) - [Webhook Relay - Careers | WebhookRelay](#webhook-relay---careers---webhookrelay) - [Privacy Policy | WebhookRelay](#privacy-policy---webhookrelay) - [Webhook Relay - Not Found | WebhookRelay](#webhook-relay---not-found---webhookrelay) - [Enterprise Software License Agreement | WebhookRelay](#enterprise-software-license-agreement---webhookrelay) - [Bin Deleted | WebhookRelay](#bin-deleted---webhookrelay) - [Webhook Relay - Contact Sales | WebhookRelay](#webhook-relay---contact-sales---webhookrelay) - [Free HMAC Generator & Signature Verifier | WebhookRelay](#free-hmac-generator---signature-verifier---webhookrelay) - [Debug Webhooks: A Practical Troubleshooting Guide (2026)](#debug-webhooks--a-practical-troubleshooting-guide--2026-) - [Webhook Authentication: HMAC, mTLS & Tokens | WebhookRelay](#webhook-authentication--hmac--mtls---tokens---webhookrelay) - [Webhook Retries and Idempotency: A Practical Guide](#webhook-retries-and-idempotency--a-practical-guide) - [Security & Tech | WebhookRelay](#security---tech---webhookrelay) - [Hookdeck Alternative for Private Delivery | WebhookRelay](#hookdeck-alternative-for-private-delivery---webhookrelay) - [Svix Alternative for Private Webhook Delivery | WebhookRelay](#svix-alternative-for-private-webhook-delivery---webhookrelay) - [Pipedream Alternative for Webhook Routing | WebhookRelay](#pipedream-alternative-for-webhook-routing---webhookrelay) - [Zapier Webhooks Alternative for Developers | WebhookRelay](#zapier-webhooks-alternative-for-developers---webhookrelay) - [Cloudflare Tunnel Alternative for Webhooks | WebhookRelay](#cloudflare-tunnel-alternative-for-webhooks---webhookrelay) - [Agent Skills | WebhookRelay](#agent-skills---webhookrelay) - [webhook.site Alternative for Webhook Testing | WebhookRelay](#webhook-site-alternative-for-webhook-testing---webhookrelay) - [MCP Server | WebhookRelay](#mcp-server---webhookrelay) - [Beeceptor Alternative for Webhooks (2026) | WebhookRelay](#beeceptor-alternative-for-webhooks--2026----webhookrelay) - [skills.sh — the Agent Skills CLI | WebhookRelay](#skills-sh---the-agent-skills-cli---webhookrelay) - [A RequestBin Alternative: Free Webhook Inspector, No Signup](#a-requestbin-alternative--free-webhook-inspector--no-signup) - [Jenkins Plugin | WebhookRelay](#jenkins-plugin---webhookrelay) - [A smee.io Alternative for Reliable Webhook Forwarding](#a-smee-io-alternative-for-reliable-webhook-forwarding) - [Terraform Atlantis | WebhookRelay](#terraform-atlantis---webhookrelay) - [Convoy Alternative for Webhook Delivery | WebhookRelay](#convoy-alternative-for-webhook-delivery---webhookrelay) - [localtunnel Alternative for Webhooks (2026) | WebhookRelay](#localtunnel-alternative-for-webhooks--2026----webhookrelay) - [Jenkins and Bitbucket | WebhookRelay](#jenkins-and-bitbucket---webhookrelay) - [An expose.dev Alternative for Webhooks and Tunnels (2026)](#an-expose-dev-alternative-for-webhooks-and-tunnels--2026-) - [Jenkins and GitHub | WebhookRelay](#jenkins-and-github---webhookrelay) - [Jira Webhooks: Setup, Payload and Security | WebhookRelay](#jira-webhooks--setup--payload-and-security---webhookrelay) - [WhatsApp Cloud API Webhooks: Setup & Tokens | WebhookRelay](#whatsapp-cloud-api-webhooks--setup---tokens---webhookrelay) - [DocuSign Connect Webhooks: Events, HMAC and Retry Behavior](#docusign-connect-webhooks--events--hmac-and-retry-behavior) - [Zendesk Webhooks: Setup, Signatures and Payload Examples](#zendesk-webhooks--setup--signatures-and-payload-examples) - [Confluence Webhooks: Your Options on Cloud and Data Center](#confluence-webhooks--your-options-on-cloud-and-data-center) - [Splunk Webhook Alerts: Payload, Limits and Fan-Out](#splunk-webhook-alerts--payload--limits-and-fan-out) - [Receive GitLab Webhooks Locally | WebhookRelay](#receive-gitlab-webhooks-locally---webhookrelay) - [Receive Twilio Webhooks Locally | WebhookRelay](#receive-twilio-webhooks-locally---webhookrelay) - [Receive Slack Events on localhost | WebhookRelay](#receive-slack-events-on-localhost---webhookrelay) - [Test SendGrid Webhooks Locally | WebhookRelay](#test-sendgrid-webhooks-locally---webhookrelay) - [Receive Square Webhooks Locally | WebhookRelay](#receive-square-webhooks-locally---webhookrelay) - [Kubernetes Operator | WebhookRelay](#kubernetes-operator---webhookrelay) - [Test Sentry Webhooks Locally | WebhookRelay](#test-sentry-webhooks-locally---webhookrelay) - [Receive Auth0 Webhooks Locally | WebhookRelay](#receive-auth0-webhooks-locally---webhookrelay) - [Test Clerk Webhooks Locally | WebhookRelay](#test-clerk-webhooks-locally---webhookrelay) - [Receive Vercel Webhooks Locally | WebhookRelay](#receive-vercel-webhooks-locally---webhookrelay) - [Test Linear Webhooks Locally | WebhookRelay](#test-linear-webhooks-locally---webhookrelay) - [Test Typeform Webhooks Locally | WebhookRelay](#test-typeform-webhooks-locally---webhookrelay) - [Test Calendly Webhooks Locally | WebhookRelay](#test-calendly-webhooks-locally---webhookrelay) - [Receive Mailgun Webhooks Locally | WebhookRelay](#receive-mailgun-webhooks-locally---webhookrelay) - [Receive Razorpay Webhooks Locally | WebhookRelay](#receive-razorpay-webhooks-locally---webhookrelay) - [Receive Mollie Webhooks Locally | WebhookRelay](#receive-mollie-webhooks-locally---webhookrelay) - [Receive Datadog Webhooks Locally | WebhookRelay](#receive-datadog-webhooks-locally---webhookrelay) - [Test WhatsApp Webhooks Locally | WebhookRelay](#test-whatsapp-webhooks-locally---webhookrelay) - [How to receive Paypal webhooks on localhost | WebhookRelay](#how-to-receive-paypal-webhooks-on-localhost---webhookrelay) - [Test WorkOS Webhooks Locally | WebhookRelay](#test-workos-webhooks-locally---webhookrelay) - [Test Plaid Webhooks Locally | WebhookRelay](#test-plaid-webhooks-locally---webhookrelay) - [Test DocuSign Webhooks Locally | WebhookRelay](#test-docusign-webhooks-locally---webhookrelay) - [Test Zoom Webhooks Locally | WebhookRelay](#test-zoom-webhooks-locally---webhookrelay) - [Test Jira Webhooks Locally | WebhookRelay](#test-jira-webhooks-locally---webhookrelay) - [Test Mailchimp Webhooks Locally | WebhookRelay](#test-mailchimp-webhooks-locally---webhookrelay) - [Test Adyen Webhooks Locally | WebhookRelay](#test-adyen-webhooks-locally---webhookrelay) - [Test Zendesk Webhooks Locally | WebhookRelay](#test-zendesk-webhooks-locally---webhookrelay) - [Test Coinbase Commerce Webhooks Locally | WebhookRelay](#test-coinbase-commerce-webhooks-locally---webhookrelay) - [Test Asana Webhooks Locally | WebhookRelay](#test-asana-webhooks-locally---webhookrelay) - [Test Box Webhooks Locally | WebhookRelay](#test-box-webhooks-locally---webhookrelay) - [Test Okta Webhooks Locally | WebhookRelay](#test-okta-webhooks-locally---webhookrelay) - [Test Dropbox Webhooks Locally | WebhookRelay](#test-dropbox-webhooks-locally---webhookrelay) - [Stripe Webhook Tester — Test & Inspect Online | WebhookRelay](#stripe-webhook-tester---test---inspect-online---webhookrelay) - [Test Webflow Webhooks Locally | WebhookRelay](#test-webflow-webhooks-locally---webhookrelay) - [GitHub Webhook Tester — Test & Inspect Online | WebhookRelay](#github-webhook-tester---test---inspect-online---webhookrelay) - [Twilio Webhook Tester — Test & Inspect Online | WebhookRelay](#twilio-webhook-tester---test---inspect-online---webhookrelay) - [Shopify Webhook Tester — Test & Inspect Online](#shopify-webhook-tester---test---inspect-online) - [GitLab Webhook Tester — Test & Inspect Online | WebhookRelay](#gitlab-webhook-tester---test---inspect-online---webhookrelay) - [Square Webhook Tester — Test & Inspect Online | WebhookRelay](#square-webhook-tester---test---inspect-online---webhookrelay) - [PayPal Webhook Tester — Test & Inspect Online | WebhookRelay](#paypal-webhook-tester---test---inspect-online---webhookrelay) - [Sentry Webhook Tester — Test & Inspect Online | WebhookRelay](#sentry-webhook-tester---test---inspect-online---webhookrelay) - [Slack Webhook Tester — Test & Inspect Online | WebhookRelay](#slack-webhook-tester---test---inspect-online---webhookrelay) - [HubSpot Webhook Tester — Test & Inspect Online](#hubspot-webhook-tester---test---inspect-online) - [Vercel Webhook Tester — Test & Inspect Online | WebhookRelay](#vercel-webhook-tester---test---inspect-online---webhookrelay) - [Clerk Webhook Tester — Test & Inspect Online | WebhookRelay](#clerk-webhook-tester---test---inspect-online---webhookrelay) - [Linear Webhook Tester — Test & Inspect Online | WebhookRelay](#linear-webhook-tester---test---inspect-online---webhookrelay) - [Intercom Webhook Tester — Test & Inspect Online](#intercom-webhook-tester---test---inspect-online) - [Calendly Webhook Tester — Test & Inspect Online](#calendly-webhook-tester---test---inspect-online) - [Typeform Webhook Tester — Test & Inspect Online](#typeform-webhook-tester---test---inspect-online) - [Razorpay Webhook Tester — Test & Inspect Online](#razorpay-webhook-tester---test---inspect-online) - [Mailgun Webhook Tester — Test & Inspect Online](#mailgun-webhook-tester---test---inspect-online) - [SendGrid Webhook Tester — Test & Inspect Online](#sendgrid-webhook-tester---test---inspect-online) - [Datadog Webhook Tester — Test & Inspect Online](#datadog-webhook-tester---test---inspect-online) - [Send a Webhook to Discord | WebhookRelay](#send-a-webhook-to-discord---webhookrelay) - [Send a Webhook to Microsoft Teams | WebhookRelay](#send-a-webhook-to-microsoft-teams---webhookrelay) - [Webhook to Telegram: Forward Any Payload | WebhookRelay](#webhook-to-telegram--forward-any-payload---webhookrelay) - [Send a Webhook to Slack | WebhookRelay](#send-a-webhook-to-slack---webhookrelay) - [Send a Webhook as Email: Forward Any Event to Your Inbox](#send-a-webhook-as-email--forward-any-event-to-your-inbox) - [Webhook to Google Sheets: Forward Any Payload | WebhookRelay](#webhook-to-google-sheets--forward-any-payload---webhookrelay) - [Send a Webhook to PagerDuty: Trigger Incidents](#send-a-webhook-to-pagerduty--trigger-incidents) - [Send a Webhook to Opsgenie | WebhookRelay](#send-a-webhook-to-opsgenie---webhookrelay) - [Send a Webhook to Datadog: Post Events From Any Source](#send-a-webhook-to-datadog--post-events-from-any-source) - [Send a Webhook to Mattermost | WebhookRelay](#send-a-webhook-to-mattermost---webhookrelay) - [Send a Webhook to HubSpot | WebhookRelay](#send-a-webhook-to-hubspot---webhookrelay) - [Send a Webhook to Salesforce | WebhookRelay](#send-a-webhook-to-salesforce---webhookrelay) - [Send a Webhook to Trello | WebhookRelay](#send-a-webhook-to-trello---webhookrelay) - [Send a Webhook to Jira | WebhookRelay](#send-a-webhook-to-jira---webhookrelay) - [How to Send Webhooks to AWS SQS (No Consumer Code)](#how-to-send-webhooks-to-aws-sqs--no-consumer-code-) - [Send a Webhook to Airtable | WebhookRelay](#send-a-webhook-to-airtable---webhookrelay) - [Send a Webhook to Notion | WebhookRelay](#send-a-webhook-to-notion---webhookrelay) - [How to Send Webhooks to Google Cloud Pub/Sub | WebhookRelay](#how-to-send-webhooks-to-google-cloud-pub-sub---webhookrelay) - [Send a Webhook to BigQuery: Stream Events Into a Table](#send-a-webhook-to-bigquery--stream-events-into-a-table) - [How to Archive Webhooks to Amazon S3 (Store Every Payload)](#how-to-archive-webhooks-to-amazon-s3--store-every-payload-) - [How to Archive Webhooks to Google Cloud Storage (GCS)](#how-to-archive-webhooks-to-google-cloud-storage--gcs-) - [Receive Emails as Webhooks: Inbound Parsing | WebhookRelay](#receive-emails-as-webhooks--inbound-parsing---webhookrelay) - [Webhook Relay Blog | WebhookRelay](#webhook-relay-blog---webhookrelay) - [Email to Notion | WebhookRelay](#email-to-notion---webhookrelay) - [Email to API | WebhookRelay](#email-to-api---webhookrelay) - [Email to Airtable | WebhookRelay](#email-to-airtable---webhookrelay) - [Email to Slack | WebhookRelay](#email-to-slack---webhookrelay) - [Email to Discord | WebhookRelay](#email-to-discord---webhookrelay) - [Email to Google Sheets | WebhookRelay](#email-to-google-sheets---webhookrelay) - [Email to Database | WebhookRelay](#email-to-database---webhookrelay) - [Email to Microsoft Teams | WebhookRelay](#email-to-microsoft-teams---webhookrelay) - [Self-Hosted CI/CD With Drone.io | WebhookRelay](#self-hosted-ci-cd-with-drone-io---webhookrelay) - [Free Standard Webhooks Signature Verifier | WebhookRelay](#free-standard-webhooks-signature-verifier---webhookrelay) - [Free Slack Webhook Signature Verifier | WebhookRelay](#free-slack-webhook-signature-verifier---webhookrelay) - [Free Stripe Webhook Signature Verifier | WebhookRelay](#free-stripe-webhook-signature-verifier---webhookrelay) - [Free Square Webhook Signature Verifier | WebhookRelay](#free-square-webhook-signature-verifier---webhookrelay) - [Free Twilio Webhook Signature Verifier | WebhookRelay](#free-twilio-webhook-signature-verifier---webhookrelay) - [Free Zendesk Webhook Signature Verifier | WebhookRelay](#free-zendesk-webhook-signature-verifier---webhookrelay) - [Receiving webhooks on localhost | WebhookRelay](#receiving-webhooks-on-localhost---webhookrelay) - [AWS SQS | WebhookRelay](#aws-sqs---webhookrelay) - [Azure | WebhookRelay](#azure---webhookrelay) - [GCP Pub/Sub | WebhookRelay](#gcp-pub-sub---webhookrelay) - [JSON encoding | WebhookRelay](#json-encoding---webhookrelay) - [Alerting from functions | WebhookRelay](#alerting-from-functions---webhookrelay) - [Outbound throttling: control webhook delivery speed](#outbound-throttling--control-webhook-delivery-speed) - [Fix TLS/SSL Handshake Errors | WebhookRelay](#fix-tls-ssl-handshake-errors---webhookrelay) - [Durable webhooks: reliable delivery with automatic retries](#durable-webhooks--reliable-delivery-with-automatic-retries) - [Multi-factor authentication (MFA) | WebhookRelay](#multi-factor-authentication--mfa----webhookrelay) - [Autostart (Linux) | WebhookRelay](#autostart--linux----webhookrelay) - [CLI | WebhookRelay](#cli---webhookrelay) - [HTTP proxy configuration | WebhookRelay](#http-proxy-configuration---webhookrelay) - [Autostart (MacOS) | WebhookRelay](#autostart--macos----webhookrelay) - [Docker Compose | WebhookRelay](#docker-compose---webhookrelay) - [Kubernetes | WebhookRelay](#kubernetes---webhookrelay) - [Docker container | WebhookRelay](#docker-container---webhookrelay) - [Autostart (Windows) | WebhookRelay](#autostart--windows----webhookrelay) - [Multiple destinations | WebhookRelay](#multiple-destinations---webhookrelay) - [Forward to public URL | WebhookRelay](#forward-to-public-url---webhookrelay) - [JWT authentication | WebhookRelay](#jwt-authentication---webhookrelay) - [Auth using request method | WebhookRelay](#auth-using-request-method---webhookrelay) - [Username and password | WebhookRelay](#username-and-password---webhookrelay) - [Make HTTP request | WebhookRelay](#make-http-request---webhookrelay) - [HMAC | WebhookRelay](#hmac---webhookrelay) - [URL Encoded Form | WebhookRelay](#url-encoded-form---webhookrelay) - [Working with time | WebhookRelay](#working-with-time---webhookrelay) - [Read, write request data | WebhookRelay](#read--write-request-data---webhookrelay) - [Multipart form to JSON | WebhookRelay](#multipart-form-to-json---webhookrelay) - [Base64, encryption | WebhookRelay](#base64--encryption---webhookrelay) - [Sending emails | WebhookRelay](#sending-emails---webhookrelay) - [Integrating into CI/CD | WebhookRelay](#integrating-into-ci-cd---webhookrelay) - [GCP BigQuery | WebhookRelay](#gcp-bigquery---webhookrelay) - [Accessing metadata | WebhookRelay](#accessing-metadata---webhookrelay) - [Response (post-delivery) functions | WebhookRelay](#response--post-delivery--functions---webhookrelay) - [Custom webhook subdomains | WebhookRelay](#custom-webhook-subdomains---webhookrelay) - [Functions | WebhookRelay](#functions---webhookrelay) - [Schedule recurring webhooks | WebhookRelay](#schedule-recurring-webhooks---webhookrelay) - [Webhook Relay Blog | WebhookRelay](#webhook-relay-blog---webhookrelay) - [Custom webhook domains | WebhookRelay](#custom-webhook-domains---webhookrelay) - [Connecting to websocket server | WebhookRelay](#connecting-to-websocket-server---webhookrelay) - [Polling webhooks with /v1/events | WebhookRelay](#polling-webhooks-with--v1-events---webhookrelay) - [Static IP Address | WebhookRelay](#static-ip-address---webhookrelay) - [CORS for webhooks | WebhookRelay](#cors-for-webhooks---webhookrelay) - [Custom response to webhooks | WebhookRelay](#custom-response-to-webhooks---webhookrelay) - [Zero Retention Buckets | WebhookRelay](#zero-retention-buckets---webhookrelay) - [Large Webhooks | WebhookRelay](#large-webhooks---webhookrelay) - [Alerts | WebhookRelay](#alerts---webhookrelay) - [Service Connections | WebhookRelay](#service-connections---webhookrelay) - [AWS S3 | WebhookRelay](#aws-s3---webhookrelay) - [AWS SNS | WebhookRelay](#aws-sns---webhookrelay) - [Email webhook payload | WebhookRelay](#email-webhook-payload---webhookrelay) - [GCP Cloud Storage | WebhookRelay](#gcp-cloud-storage---webhookrelay) - [Sender filtering & policy | WebhookRelay](#sender-filtering---policy---webhookrelay) - [Create & poll email addresses from the CLI | WebhookRelay](#create---poll-email-addresses-from-the-cli---webhookrelay) - [Account management | WebhookRelay](#account-management---webhookrelay) - [Billing & subscriptions | WebhookRelay](#billing---subscriptions---webhookrelay) - [Webhook Relay on ClawHub | WebhookRelay](#webhook-relay-on-clawhub---webhookrelay) - [Teams and sub-accounts | WebhookRelay](#teams-and-sub-accounts---webhookrelay) - [Demoing your website | WebhookRelay](#demoing-your-website---webhookrelay) - [Regions | WebhookRelay](#regions---webhookrelay) - [n8n Email Trigger (Inbound Email) | WebhookRelay](#n8n-email-trigger--inbound-email----webhookrelay) - [Team Member Roles | WebhookRelay](#team-member-roles---webhookrelay) - [n8n WhatsApp Cloud API Webhook Setup | WebhookRelay](#n8n-whatsapp-cloud-api-webhook-setup---webhookrelay) - [Jenkins Multibranch Pipelines | WebhookRelay](#jenkins-multibranch-pipelines---webhookrelay) - [Execute scripts on webhook | WebhookRelay](#execute-scripts-on-webhook---webhookrelay) - [n8n Webhook Trigger (No Public IP) | WebhookRelay](#n8n-webhook-trigger--no-public-ip----webhookrelay) - [Webhook Relay - Features | WebhookRelay](#webhook-relay---features---webhookrelay) - [Node-RED | WebhookRelay](#node-red---webhookrelay) - [Webhook Relay - Thank you | WebhookRelay](#webhook-relay---thank-you---webhookrelay) - [Home Assistant | WebhookRelay](#home-assistant---webhookrelay) - [Free DocuSign Webhook Signature Verifier | WebhookRelay](#free-docusign-webhook-signature-verifier---webhookrelay) - [Durable Webhook Retries: A Live Demo | WebhookRelay](#durable-webhook-retries--a-live-demo---webhookrelay) - [Free Klarna Webhook Signature Verifier | WebhookRelay](#free-klarna-webhook-signature-verifier---webhookrelay) - [Free Airwallex Webhook Signature Verifier | WebhookRelay](#free-airwallex-webhook-signature-verifier---webhookrelay) - [JavaScript app | WebhookRelay](#javascript-app---webhookrelay) - [Free Attio Webhook Signature Verifier | WebhookRelay](#free-attio-webhook-signature-verifier---webhookrelay) - [GCP BigQuery | WebhookRelay](#gcp-bigquery---webhookrelay) - [Free GitHub Webhook Signature Verifier | WebhookRelay](#free-github-webhook-signature-verifier---webhookrelay) - [Free LINE Webhook Signature Verifier | WebhookRelay](#free-line-webhook-signature-verifier---webhookrelay) - [Enrich webhooks from APIs | WebhookRelay](#enrich-webhooks-from-apis---webhookrelay) - [Free MyFatoorah Webhook Signature Verifier | WebhookRelay](#free-myfatoorah-webhook-signature-verifier---webhookrelay) - [Free Shopify Webhook Signature Verifier | WebhookRelay](#free-shopify-webhook-signature-verifier---webhookrelay) - [DockerHub webhook to Slack notification | WebhookRelay](#dockerhub-webhook-to-slack-notification---webhookrelay) - [TradingView Webhooks to Discord & Slack | WebhookRelay](#tradingview-webhooks-to-discord---slack---webhookrelay) - [Controlling TV with Google Home, IFTTT and Node-RED](#controlling-tv-with-google-home--ifttt-and-node-red) - [Webhook vs WebSocket: What's the Difference? | WebhookRelay](#webhook-vs-websocket--what-s-the-difference----webhookrelay) - [Receive Shopify webhooks on Flask API | WebhookRelay](#receive-shopify-webhooks-on-flask-api---webhookrelay) - [Test HubSpot Webhooks Locally | WebhookRelay](#test-hubspot-webhooks-locally---webhookrelay) - [Test Intercom Webhooks Locally | WebhookRelay](#test-intercom-webhooks-locally---webhookrelay) - [Receive Github webhooks on Jenkins without public IP](#receive-github-webhooks-on-jenkins-without-public-ip) - [How to Get a Slack Webhook URL (Incoming Webhooks)](#how-to-get-a-slack-webhook-url--incoming-webhooks-) - [How to Get a Discord Webhook URL | WebhookRelay](#how-to-get-a-discord-webhook-url---webhookrelay) - [How to Create a Microsoft Teams Webhook (Workflows, 2026)](#how-to-create-a-microsoft-teams-webhook--workflows--2026-) - [How to Send a Webhook from Google Forms (Apps Script)](#how-to-send-a-webhook-from-google-forms--apps-script-) - [Webhook Architecture Diagram: How Webhooks Flow End to End](#webhook-architecture-diagram--how-webhooks-flow-end-to-end) - [Webhooks in CI/CD: GitHub Actions & Jenkins | WebhookRelay](#webhooks-in-ci-cd--github-actions---jenkins---webhookrelay) - [Secure webhooks to Jenkins on Kubernetes | WebhookRelay](#secure-webhooks-to-jenkins-on-kubernetes---webhookrelay) - [Twilio Webhooks: Setup, Payloads & Signatures | WebhookRelay](#twilio-webhooks--setup--payloads---signatures---webhookrelay) - [Mailgun webhook fan-out | WebhookRelay](#mailgun-webhook-fan-out---webhookrelay) - [Receive emails on new Stripe subscribers | WebhookRelay](#receive-emails-on-new-stripe-subscribers---webhookrelay) - [Introducing Service Connections | WebhookRelay](#introducing-service-connections---webhookrelay) - [Trigger a Self-Hosted AI Agent with Webhooks (No Public IP)](#trigger-a-self-hosted-ai-agent-with-webhooks--no-public-ip-) - [Test Stripe Connect Webhooks Locally | WebhookRelay](#test-stripe-connect-webhooks-locally---webhookrelay) - [Stripe CLI Alternative for Webhook Testing | WebhookRelay](#stripe-cli-alternative-for-webhook-testing---webhookrelay) - [Archive AWS GuardDuty Findings to GCP Cloud Storage](#archive-aws-guardduty-findings-to-gcp-cloud-storage) - [Introducing extra webhook packages | WebhookRelay](#introducing-extra-webhook-packages---webhookrelay) - [Azure Functions vs Webhook Relay | WebhookRelay](#azure-functions-vs-webhook-relay---webhookrelay) - [How Lightning AI Ships Webhooks Team-Wide | WebhookRelay](#how-lightning-ai-ships-webhooks-team-wide---webhookrelay) - [Automatically transform webhook payloads | WebhookRelay](#automatically-transform-webhook-payloads---webhookrelay) - [Airtable integrations: inserting rows | WebhookRelay](#airtable-integrations--inserting-rows---webhookrelay) - [GKE Control-Plane Failure: May 2022 Outage | WebhookRelay](#gke-control-plane-failure--may-2022-outage---webhookrelay) - [Static IPs for outgoing webhooks | WebhookRelay](#static-ips-for-outgoing-webhooks---webhookrelay) - [Run Dockerized Jenkins CI With Webhooks | WebhookRelay](#run-dockerized-jenkins-ci-with-webhooks---webhookrelay) - [Changes to our prices for new customers | WebhookRelay](#changes-to-our-prices-for-new-customers---webhookrelay) - [Self-hosted business intelligence with Metabase](#self-hosted-business-intelligence-with-metabase) - [Kubernetes Access Through Tunnels — Case Study](#kubernetes-access-through-tunnels---case-study) - [Ingesting Facebook webhooks (challenge & verification)](#ingesting-facebook-webhooks--challenge---verification-) - [Running Webhook Relay agent with Podman | WebhookRelay](#running-webhook-relay-agent-with-podman---webhookrelay) - [CDN types and setting them up (Vue, React) | WebhookRelay](#cdn-types-and-setting-them-up--vue--react----webhookrelay) - [New feature announcement: domain-based endpoints](#new-feature-announcement--domain-based-endpoints) - [Dotscience: Tunnels at Scale for Data Science | WebhookRelay](#dotscience--tunnels-at-scale-for-data-science---webhookrelay) - [Static IPs for webhook calls to enable whitelisting](#static-ips-for-webhook-calls-to-enable-whitelisting) - [Responding to API calls using Node-RED Webhook Relay node](#responding-to-api-calls-using-node-red-webhook-relay-node) - [Docker Compose update on Github webhook | WebhookRelay](#docker-compose-update-on-github-webhook---webhookrelay) - [Automated Jenkins builds on GitHub pull request](#automated-jenkins-builds-on-github-pull-request) - [Rules-based webhook filtering & routing | WebhookRelay](#rules-based-webhook-filtering---routing---webhookrelay) - [Using Google Firestore for a Golang backend application](#using-google-firestore-for-a-golang-backend-application) - [Cloudflare Support for Home Assistant | WebhookRelay](#cloudflare-support-for-home-assistant---webhookrelay) - [Node-RED OwnTracks location tracking without public IP/MQTT](#node-red-owntracks-location-tracking-without-public-ip-mqtt) - [Remote YouTube downloader Slack bot | WebhookRelay](#remote-youtube-downloader-slack-bot---webhookrelay) - [Introducing WebSocket Server | WebhookRelay](#introducing-websocket-server---webhookrelay) - [Rancher - push to deploy workflow with Keel | WebhookRelay](#rancher---push-to-deploy-workflow-with-keel---webhookrelay) - [Home Assistant remote access add-on | WebhookRelay](#home-assistant-remote-access-add-on---webhookrelay) - [Documenting your API with OpenAPI (Swagger) and Redoc](#documenting-your-api-with-openapi--swagger--and-redoc) - [Home Assistant Remote Access on Raspberry Pi | WebhookRelay](#home-assistant-remote-access-on-raspberry-pi---webhookrelay) - [DevOps Use Case: Performing Redis maintenance in Kubernetes](#devops-use-case--performing-redis-maintenance-in-kubernetes) - [Keel - automated Kubernetes updates | WebhookRelay](#keel---automated-kubernetes-updates---webhookrelay) - [Web Relay Ingress with Docker for Mac | WebhookRelay](#web-relay-ingress-with-docker-for-mac---webhookrelay) - [Auto deploy your Node.js app on push to GitHub](#auto-deploy-your-node-js-app-on-push-to-github) - [Introduction to Webhook Relay | WebhookRelay](#introduction-to-webhook-relay---webhookrelay) - [TLS Compatibility: Custom & Legacy TLS Versions](#tls-compatibility--custom---legacy-tls-versions) - [Changelog | WebhookRelay](#changelog---webhookrelay) --- --- title: Webhook Relay - forward webhooks to any destination meta: "og: title": WebhookRelay description: Webhook Relay is a simple, flexible and high performance webhook routing system for internal and public destinations. Transform, route and retry webhooks. url: https://webhookrelay.com/home-classic.md file: /home-classic.md --- # **Forward webhooks anywhere**in minutes Receive, transform and deliver webhooks to any destination — a public URL, a private server, or your localhost. No firewall changes, no missed events. [Start forwarding free](https://my.webhookrelay.com/register) [ AI Agents](https://webhookrelay.com/home-classic/docs/skills.md "Agent Skills — teach Claude and other AI agents to drive Webhook Relay") Free plan · 150 webhooks/month · No credit card required ![Avatar 01](https://webhookrelay.com/home-classic/images/rusenask-avatar.jpeg)![Avatar 02](https://webhookrelay.com/home-classic/images/luca.png)![Avatar 03](https://webhookrelay.com/home-classic/images/avatar-03.jpg)![Avatar 04](https://webhookrelay.com/home-classic/images/avatar-04.jpg)![Avatar 05](https://webhookrelay.com/home-classic/images/avatar-05.jpg) **40,000+** professionals love us • Trusted by **500 Fortune companies** ![Dashboard screenshot](https://webhookrelay.com/home-classic/images/dashboard.png) ![WebhookRelay Logo](https://webhookrelay.com/home-classic/images/logo.png) ![Public Destination Logo](https://webhookrelay.com/home-classic/images/logo-02.svg) ![On-Prem Server Logo](https://webhookrelay.com/home-classic/images/jenkins_logo.png) ![Laptop Logo](https://webhookrelay.com/home-classic/images/logo-04.svg) ![Enterprise Logo](https://webhookrelay.com/home-classic/images/logo-05.svg) ![Logo 06](https://webhookrelay.com/home-classic/images/logo-06.svg) ![Logo 07](https://webhookrelay.com/home-classic/images/logo-07.svg) ![Logo 08](https://webhookrelay.com/home-classic/images/logo-08.svg) ![Logo 09](https://webhookrelay.com/home-classic/images/logo-09.svg) ### **How it works (public destinations)** **1.** #### **Enter destinations** Specify one or more public URLs where you want your webhooks forwarded. **2.** #### **Enable features (optinal)** Activate functionalities like authentication, transformation, static IP address, data warehouse ingestion, etc., as needed. **3.** #### **Start sending webhooks** Begin sending webhooks to your public Webhook Relay endpoint. We'll handle the delivery. Your browser does not support the video tag. Forwarding webhooks to public destinations ### **How it works (private destinations)** **1.** #### **Enter destinations** Specify one or more internal URLs (e.g., `http://localhost:8080/webhook`) for your webhooks. **2.** #### **Setup the agent** Install and run the `relay` agent (CLI or Docker) on your network. Check connection status in your dashboard. **3.** #### **Start sending webhooks** Begin sending webhooks to your public Webhook Relay endpoint. The agent will securely tunnel them to your private destinations. Your browser does not support the video tag. Forwarding webhooks to private destinations ![Large testimonial](https://webhookrelay.com/home-classic/images/luca.png) **"Webhook Relay has become a key piece of our infrastructure. It allowed us to simplify our local development story and **_*enable complex CI pipelines*_**. Good tooling makes your life easier and Webhook Relay nails it.”** Luca Antiga** **/** **[CTO at Lightning AI](https://lightning.ai) ## **Webhook Relay helps your teams work more efficiently and ship faster** ![Planet](https://webhookrelay.com/home-classic/images/planet.png) ![Planet decoration](https://webhookrelay.com/home-classic/images/planet-overlay.svg) ![Tag 01](https://webhookrelay.com/home-classic/images/planet-tag-01.png)![Tag 02](https://webhookrelay.com/home-classic/images/planet-tag-02.png)![Tag 03](https://webhookrelay.com/home-classic/images/planet-tag-03.png)![Tag 04](https://webhookrelay.com/home-classic/images/planet-tag-04.png) ### Secure on-prem CI/CD Securely forward webhooks from your CI/CD pipelines to your local services without exposing your private IP. Works with GitHub, GitLab, Jenkins, CircleCI, and more. ### Warehouse integration Forward webhooks to your warehouse of choice. Such as Snowflake, BigQuery, Databricks, and more. ### Custom Code Use Functions to enable sharing info between your apps. Transform requests to accommodate target's API structure. ### Kubernetes Operator We provide Kubernetes operator for webhook forwarding and ingress controller for bidirectional tunnelling. ### Scheduled Webhooks Schedule webhooks to be sent at a specific time in any timezone. Webhooks can be sent on a daily, weekly, monthly or yearly basis. ### Execute scripts on webhooks Simply execute any scripts or commands on machine when a webhook is received. ## **Validate work much faster** Share your work-in-progress website directly with your peers. Test webhooks, 3rd party integrations with a backend running locally on your laptop. **AI Project** relay connect http://localhost:4000Connected https://xyz.webrelay.io -> http://localhost:4000relay forward -b stripeForwarding https://stripe.webrelay.io -> http://localhost:4000 ![Overlay 01](https://webhookrelay.com/home-classic/images/features-02-overlay-01.png) ![Overlay 02](https://webhookrelay.com/home-classic/images/features-02-overlay-02.png) ![Overlay 03](https://webhookrelay.com/home-classic/images/features-02-overlay-03.png) ### Early feedback Collect essential feedback from your peers about your work-in-progress website. Test payments, webhooks without deploying to production. ### Secure webhooks Avoid exposing your CI/CD systems such as Jenkins to the public internet. Forward webhooks to your on-prem servers securely. Webhook Relay allows one-way communication. ### Custom Domains Our paid plan include custom subdomains and domains so you can use your own domain names. ![Stripes](https://webhookrelay.com/home-classic/images/stripes-dark.svg) ## **Start forwarding webhooks in minutes ** Connect a source, pick a destination, and Webhook Relay handles delivery, retries and transforms. Set up your first webhook in under five minutes. [Start for free ->](https://my.webhookrelay.com/register) Free plan · No credit card required · 7-day money-back guarantee on paid plans --- --- title: Webhook Guides & Resources | WebhookRelay meta: "og: title": "Webhook Guides & Resources" description: Webhook guides and tutorials: what webhooks are, how to test, secure, verify and route them, and how to receive Stripe or GitHub webhooks on localhost. url: https://webhookrelay.com/guides.md file: /guides.md --- # **Webhook Guides & Resources ** Everything you need to receive, test, secure, transform and route webhooks — plus tutorials for the providers and destinations you actually use. [Start forwarding for free](https://my.webhookrelay.com/register) ## **Webhook fundamentals** Start here if you are new to webhooks. [**W****What is a Webhook?**Definition, examples and how to set one up.](https://webhookrelay.com/guides/blog/what-is-webhook/) [**W****Webhooks vs API**Push vs pull, and when to use each.](https://webhookrelay.com/guides/blog/webhooks-vs-api/) [**W****What is a Webhook Gateway?**The managed layer between producers and your services.](https://webhookrelay.com/guides/webhook-gateway/) ## **Drive webhooks from AI agents (Skills & MCP)** Let Claude, Cursor and other agents forward, transform and debug webhooks — install the open-source Skills or connect over MCP. [**A****Agent Skills**Install with npx skills add webhookrelay/skills.](https://webhookrelay.com/guides/docs/skills/) [**M****MCP Server**Typed tools for buckets, logs and transform functions.](https://webhookrelay.com/guides/docs/mcp/) [**S****Skills on GitHub**github.com/webhookrelay/skills — browse & contribute.](https://github.com/webhookrelay/skills) [**S****skills.sh CLI**The open-source CLI to install the skills — with an example.](https://webhookrelay.com/guides/docs/skills-cli/) [**W****Webhook Bin MCP**Create bins, inspect requests & run transforms from your agent.](https://webhookrelay.com/guides/webhook-bin/) ## **CI/CD — trigger Jenkins & more** Receive SCM webhooks on build servers behind a firewall or without a public IP. [**J****Jenkins Plugin**Native plugin: GitHub/GitLab/Bitbucket webhooks without exposing Jenkins.](https://webhookrelay.com/guides/docs/tutorials/cicd/jenkins-plugin/) [**J****Jenkins + GitHub (CLI)**Forward GitHub webhooks to Jenkins with the relay agent.](https://webhookrelay.com/guides/docs/tutorials/cicd/jenkins-github/) [**J****Jenkins + Bitbucket (CLI)**Forward Bitbucket webhooks to Jenkins with the relay agent.](https://webhookrelay.com/guides/docs/tutorials/cicd/jenkins-bitbucket/) [**K****Kubernetes Operator**Manage tunnels and forwarding from Kubernetes.](https://webhookrelay.com/guides/docs/tutorials/cicd/kubernetes-operator/) [**T****Terraform Atlantis**Drive Atlantis plan/apply from VCS webhooks.](https://webhookrelay.com/guides/docs/tutorials/cicd/terraform-atlantis/) ## **Test & debug webhooks** [**H****How to Test Webhooks**Inspect, forward to localhost, replay.](https://webhookrelay.com/guides/blog/how-to-test-webhooks/) [**H****How to Debug Webhooks**Why deliveries fail and how to fix them.](https://webhookrelay.com/guides/blog/how-to-debug-webhooks/) [**F****Free Webhook Tester**Instant URL to inspect requests in your browser.](https://webhookrelay.com/guides/webhook-bin/) [**H****HMAC Generator & Verifier**Compute and check webhook signatures.](https://webhookrelay.com/guides/hmac-verification/) ## **Secure & reliable webhooks** [**W****Webhook Security Best Practices**The full checklist.](https://webhookrelay.com/guides/blog/webhook-security/) [**V****Verify a Webhook Signature**HMAC/SHA256, per provider.](https://webhookrelay.com/guides/blog/verify-webhook-signature/) [**W****Webhook Authentication Methods**HMAC, tokens, mTLS, IP allow-listing.](https://webhookrelay.com/guides/blog/webhook-authentication/) [**R****Retries & Idempotency**Handle failures and duplicates safely.](https://webhookrelay.com/guides/blog/webhook-retries-and-idempotency/) ## **Compare webhook & tunnel tools** Honest comparisons with the alternatives. [**N****ngrok alternative**](https://webhookrelay.com/guides/blog/ngrok-alternative/) [**H****Hookdeck alternative**](https://webhookrelay.com/guides/blog/hookdeck-alternative/) [**S****Svix alternative**](https://webhookrelay.com/guides/blog/svix-alternative/) [**P****Pipedream alternative**](https://webhookrelay.com/guides/blog/pipedream-alternative/) [**Z****Zapier webhooks alternative**](https://webhookrelay.com/guides/blog/zapier-webhooks-alternative/) [**C****Cloudflare Tunnel alternative**](https://webhookrelay.com/guides/blog/cloudflare-tunnel-alternative/) [**W****webhook.site alternative**](https://webhookrelay.com/guides/blog/webhook-site-alternative/) [**R****RequestBin alternative**](https://webhookrelay.com/guides/blog/requestbin-alternative/) [**B****Beeceptor alternative**](https://webhookrelay.com/guides/blog/beeceptor-alternative/) [**S****smee.io alternative**](https://webhookrelay.com/guides/blog/smee-io-alternative/) [**C****Convoy alternative**](https://webhookrelay.com/guides/blog/convoy-alternative/) [**L****localtunnel alternative**](https://webhookrelay.com/guides/blog/localtunnel-alternative/) [**E****expose.dev alternative**](https://webhookrelay.com/guides/blog/expose-dev-alternative/) ## **Provider webhook guides** Deep dives into how specific providers send, sign and retry webhooks. [**Jira**Setup, payload, JQL filters and X-Hub-Signature security.](https://webhookrelay.com/guides/blog/jira-webhooks-guide/) [**WhatsApp**Cloud API setup, verify token handshake and payloads.](https://webhookrelay.com/guides/blog/whatsapp-cloud-api-webhooks/) [**DocuSign**Connect events, HMAC and its aggressive retry behavior.](https://webhookrelay.com/guides/blog/docusign-connect-webhooks/) [**Zendesk**Triggers vs event subscriptions, signatures and retries.](https://webhookrelay.com/guides/blog/zendesk-webhooks-guide/) [**Confluence**Automation web requests, Forge apps and Data Center webhooks.](https://webhookrelay.com/guides/blog/confluence-webhooks/) [**Splunk**Alert action payloads, limits and fan-out patterns.](https://webhookrelay.com/guides/blog/splunk-webhook-alerts/) ## **Receive provider webhooks on localhost** Test real webhooks against your local code, no deploy needed. [**Stripe**](https://webhookrelay.com/guides/blog/receiving-stripe-webhooks-localhost/) [**GitHub**](https://webhookrelay.com/guides/blog/receive-github-webhooks-locally/) [**GitLab**](https://webhookrelay.com/guides/blog/receive-gitlab-webhooks-locally/) [**Twilio**](https://webhookrelay.com/guides/blog/receive-twilio-webhooks-locally/) [**Slack**](https://webhookrelay.com/guides/blog/receive-slack-events-locally/) [**Square**](https://webhookrelay.com/guides/blog/receive-square-webhooks-locally/) [**SendGrid**](https://webhookrelay.com/guides/blog/receive-sendgrid-webhooks-locally/) [**Sentry**](https://webhookrelay.com/guides/blog/receive-sentry-webhooks-locally/) [**Clerk**](https://webhookrelay.com/guides/blog/receive-clerk-webhooks-locally/) [**Auth0**](https://webhookrelay.com/guides/blog/receive-auth0-webhooks-locally/) [**Vercel**](https://webhookrelay.com/guides/blog/receive-vercel-webhooks-locally/) [**Linear**](https://webhookrelay.com/guides/blog/receive-linear-webhooks-locally/) [**Calendly**](https://webhookrelay.com/guides/blog/receive-calendly-webhooks-locally/) [**T****Typeform**](https://webhookrelay.com/guides/blog/receive-typeform-webhooks-locally/) [**Mailgun**](https://webhookrelay.com/guides/blog/receive-mailgun-webhooks-locally/) [**Datadog**](https://webhookrelay.com/guides/blog/receive-datadog-webhooks-locally/) [**Razorpay**](https://webhookrelay.com/guides/blog/receive-razorpay-webhooks-locally/) [**M****Mollie**](https://webhookrelay.com/guides/blog/receive-mollie-webhooks-locally/) [**WhatsApp**](https://webhookrelay.com/guides/blog/receive-whatsapp-webhooks-locally/) [**PayPal**](https://webhookrelay.com/guides/blog/receiving-paypal-webhooks-localhost/) [**W****WorkOS**](https://webhookrelay.com/guides/blog/receive-workos-webhooks-locally/) [**P****Plaid**](https://webhookrelay.com/guides/blog/receive-plaid-webhooks-locally/) [**DocuSign**](https://webhookrelay.com/guides/blog/receive-docusign-webhooks-locally/) [**Z****Zoom**](https://webhookrelay.com/guides/blog/receive-zoom-webhooks-locally/) [**Jira**](https://webhookrelay.com/guides/blog/receive-jira-webhooks-locally/) [**Mailchimp**](https://webhookrelay.com/guides/blog/receive-mailchimp-webhooks-locally/) [**A****Adyen**](https://webhookrelay.com/guides/blog/receive-adyen-webhooks-locally/) [**C****Coinbase Commerce**](https://webhookrelay.com/guides/blog/receive-coinbase-commerce-webhooks-locally/) [**Zendesk**](https://webhookrelay.com/guides/blog/receive-zendesk-webhooks-locally/) [**Asana**](https://webhookrelay.com/guides/blog/receive-asana-webhooks-locally/) [**B****Box**](https://webhookrelay.com/guides/blog/receive-box-webhooks-locally/) [**Dropbox**](https://webhookrelay.com/guides/blog/receive-dropbox-webhooks-locally/) [**Okta**](https://webhookrelay.com/guides/blog/receive-okta-webhooks-locally/) [**Webflow**](https://webhookrelay.com/guides/blog/receive-webflow-webhooks-locally/) ## **Test provider webhooks online** Capture and inspect real provider payloads in your browser with a free webhook tester URL. [**Stripe Webhook Tester**](https://webhookrelay.com/guides/blog/stripe-webhook-tester/) [**GitHub Webhook Tester**](https://webhookrelay.com/guides/blog/github-webhook-tester/) [**Shopify Webhook Tester**](https://webhookrelay.com/guides/blog/shopify-webhook-tester/) [**Twilio Webhook Tester**](https://webhookrelay.com/guides/blog/twilio-webhook-tester/) [**GitLab Webhook Tester**](https://webhookrelay.com/guides/blog/gitlab-webhook-tester/) [**Square Webhook Tester**](https://webhookrelay.com/guides/blog/square-webhook-tester/) [**Sentry Webhook Tester**](https://webhookrelay.com/guides/blog/sentry-webhook-tester/) [**PayPal Webhook Tester**](https://webhookrelay.com/guides/blog/paypal-webhook-tester/) [**Slack Webhook Tester**](https://webhookrelay.com/guides/blog/slack-webhook-tester/) [**Clerk Webhook Tester**](https://webhookrelay.com/guides/blog/clerk-webhook-tester/) [**Vercel Webhook Tester**](https://webhookrelay.com/guides/blog/vercel-webhook-tester/) [**HubSpot Webhook Tester**](https://webhookrelay.com/guides/blog/hubspot-webhook-tester/) [**Linear Webhook Tester**](https://webhookrelay.com/guides/blog/linear-webhook-tester/) [**Intercom Webhook Tester**](https://webhookrelay.com/guides/blog/intercom-webhook-tester/) [**T****Typeform Webhook Tester**](https://webhookrelay.com/guides/blog/typeform-webhook-tester/) [**Calendly Webhook Tester**](https://webhookrelay.com/guides/blog/calendly-webhook-tester/) [**Razorpay Webhook Tester**](https://webhookrelay.com/guides/blog/razorpay-webhook-tester/) [**Mailgun Webhook Tester**](https://webhookrelay.com/guides/blog/mailgun-webhook-tester/) [**SendGrid Webhook Tester**](https://webhookrelay.com/guides/blog/sendgrid-webhook-tester/) [**Datadog Webhook Tester**](https://webhookrelay.com/guides/blog/datadog-webhook-tester/) ## **Route webhooks anywhere** Transform and forward webhooks to the destinations you use. [**W****Webhook to Discord**](https://webhookrelay.com/guides/blog/webhook-to-discord/) [**W****Webhook to Slack**](https://webhookrelay.com/guides/blog/webhook-to-slack/) [**W****Webhook to Microsoft Teams**](https://webhookrelay.com/guides/blog/webhook-to-microsoft-teams/) [**W****Webhook to Telegram**](https://webhookrelay.com/guides/blog/webhook-to-telegram/) [**W****Webhook to Email**](https://webhookrelay.com/guides/blog/webhook-to-email/) [**W****Webhook to Google Sheets**](https://webhookrelay.com/guides/blog/webhook-to-google-sheets/) [**W****Webhook to PagerDuty**](https://webhookrelay.com/guides/blog/webhook-to-pagerduty/) [**W****Webhook to Opsgenie**](https://webhookrelay.com/guides/blog/webhook-to-opsgenie/) [**W****Webhook to Datadog**](https://webhookrelay.com/guides/blog/webhook-to-datadog/) [**W****Webhook to Mattermost**](https://webhookrelay.com/guides/blog/webhook-to-mattermost/) [**W****Webhook to HubSpot**](https://webhookrelay.com/guides/blog/webhook-to-hubspot/) [**W****Webhook to Salesforce**](https://webhookrelay.com/guides/blog/webhook-to-salesforce/) [**W****Webhook to Jira**](https://webhookrelay.com/guides/blog/webhook-to-jira/) [**W****Webhook to Trello**](https://webhookrelay.com/guides/blog/webhook-to-trello/) [**W****Webhook to Notion**](https://webhookrelay.com/guides/blog/webhook-to-notion/) [**W****Webhook to Airtable**](https://webhookrelay.com/guides/blog/webhook-to-airtable/) [**W****Webhook to AWS SQS**](https://webhookrelay.com/guides/blog/webhook-to-sqs/) [**W****Webhook to Google Pub/Sub**](https://webhookrelay.com/guides/blog/webhook-to-pubsub/) [**W****Webhook to BigQuery**](https://webhookrelay.com/guides/blog/webhook-to-bigquery/) [**A****Archive webhooks to S3**](https://webhookrelay.com/guides/blog/webhook-to-s3/) [**A****Archive webhooks to GCS**](https://webhookrelay.com/guides/blog/webhook-to-gcs/) ## **Receive email as webhooks** Turn inbound email into webhooks and route it anywhere. [**E****Email to Webhook (inbound email API)**](https://webhookrelay.com/guides/email-to-webhook/) [**R****Receive emails as webhooks (overview)**](https://webhookrelay.com/guides/blog/receive-emails-as-webhooks/) [**E****Email docs & payload reference**](https://webhookrelay.com/guides/docs/email/) [**E****Email to Slack**](https://webhookrelay.com/guides/docs/tutorials/email/slack/) [**E****Email to Discord**](https://webhookrelay.com/guides/docs/tutorials/email/discord/) [**E****Email to Microsoft Teams**](https://webhookrelay.com/guides/docs/tutorials/email/microsoft-teams/) [**E****Email to Google Sheets**](https://webhookrelay.com/guides/docs/tutorials/email/google-sheets/) [**E****Email to Airtable**](https://webhookrelay.com/guides/docs/tutorials/email/airtable/) [**E****Email to Notion**](https://webhookrelay.com/guides/docs/tutorials/email/notion/) [**E****Email to API**](https://webhookrelay.com/guides/docs/tutorials/email/api/) [**E****Email to Database**](https://webhookrelay.com/guides/docs/tutorials/email/database/) --- --- title: Localhost Tunnels — Expose Your Local Server | WebhookRelay meta: "og: title": "Localhost Tunnels — Expose Your Local Server" description: Expose a local or internal server with a stable public HTTPS or TCP tunnel — a reliable ngrok alternative for APIs, web apps and webhook receivers. url: https://webhookrelay.com/tunnels.md file: /tunnels.md --- # **Tunnels for localhost** Expose servers from your local machine. Serve backend APIs, frontend apps and AI models. [New Tunnel ->](https://my.webhookrelay.com/new-tunnel) [Learn More](https://webhookrelay.com/tunnels/docs/webhooks/internal/localhost) ## **Domains that do not expire** Webhook Relay tunnels are not expiring. You can keep them forever. Configure the tunnel once, connect to it at any time. ![Cron](https://webhookrelay.com/tunnels/images/landing/tunnels/list.png) ### Encrypted tunnels Webhook Relay tunnels are encrypted. Securely expose your local servers to the internet. ### Custom subdomains Customize your tunnel domain with a subdomain. ### Global regions Webhook Relay tunnels are available in multiple regions. Connect to the closest region to your users. ![Stripes](https://webhookrelay.com/tunnels/images/stripes-dark.svg) ## **Start forwarding webhooks in minutes ** Connect a source, pick a destination, and Webhook Relay handles delivery, retries and transforms. Set up your first webhook in under five minutes. [Start for free ->](https://my.webhookrelay.com/register) Free plan · No credit card required · 7-day money-back guarantee on paid plans --- --- title: The State of Webhooks — live webhook traffic data meta: "og: title": "The State of Webhooks — live webhook traffic data" description: A live, privacy-preserving view of real webhook traffic: the share of providers, HTTP clients, languages, countries and TLS adoption. Aggregates only. url: https://webhookrelay.com/state-of-webhooks.md file: /state-of-webhooks.md --- # **The State of Webhooks ** A live, privacy-preserving look at real webhook traffic through Webhook Relay — which providers send them, the HTTP clients and languages behind them, and where they come from. Every figure is a ****share of traffic****; aggregate fingerprints only, no payloads, headers or customer data. Platform + public bin, unioned Providers **45** identified HTTP clients **18** fingerprinted Languages **8** detected TLS 1.3 **88%** of connections POST **98%** of requests ### **Top webhook providers** Share of traffic by sending service - AWS EventBridge**36.3%** - Juniper Mist**22.6%** - GitHub**12.1%** - Adyen**11.9%** - Splunk**7.2%** - SendGrid**4.6%** - WAHA**2.2%** - Stripe**0.6%** - Atlassian**0.6%** - Vesta**0.4%** - Svix**0.3%** - Uptime Kuma**0.2%** Top 12 of 45 identified providers ### **HTTP clients** Share of traffic by client library or runtime - Go**55.1%** - Java**30.6%** - Python (urllib)**9.3%** - Axios**2.3%** - OkHttp**0.6%** - Node.js**0.6%** - Requests**0.5%** - Ruby**0.4%** - cURL**0.2%** - PHP**0.2%** - Browser**0.1%** - Deno**<0.1%** ### **Provider share over time** Top 10 providers + other — each row is one collection interval (adds to 100%) - GitHub - Adyen - AWS EventBridge - Juniper Mist - Splunk - SendGrid - Stripe - WAHA - Atlassian - Svix - Other Jul 8 05h Jul 8 12h Jul 8 16h Jul 9 00h Jul 9 04h Jul 9 08h Jul 9 12h Jul 9 16h Jul 9 20h Jul 10 00h Jul 10 04h Jul 10 08h Jul 10 12h Jul 10 16h Jul 10 20h Jul 11 00h Jul 11 04h Jul 11 08h Jul 11 12h Jul 11 16h Jul 11 20h Jul 12 00h Jul 12 04h Jul 12 08h Jul 12 12h Jul 12 16h Jul 12 20h Jul 13 00h Jul 13 04h Jul 13 08h Jul 13 12h Jul 13 16h Jul 13 20h Jul 14 00h Jul 14 04h Jul 14 08h Jul 14 12h Jul 19 10h Jul 19 12h Jul 19 16h Jul 19 20h Jul 20 00h Jul 20 04h Jul 20 08h Jul 20 12h Jul 20 16h Jul 20 20h Jul 21 00h Jul 21 04h Jul 21 08h Jul 21 12h Jul 21 16h Jul 21 20h Jul 22 00h Jul 22 04h Jul 22 08h Jul 22 12h Jul 25 20h Jul 26 00h Jul 26 04h Jul 26 08h Jul 26 12h Jul 26 16h Jul 26 20h Jul 27 00h Jul 27 04h Jul 27 08h Jul 27 12h Jul 27 16h Jul 27 20h Jul 28 00h Jul 28 04h Jul 28 08h Jul 28 12h Jul 28 16h Jul 28 20h Jul 29 00h Jul 29 04h Jul 29 08h Jul 29 12h Jul 29 16h Jul 29 20h Jul 30 00h Jul 30 04h Jul 30 08h Jul 30 12h Jul 30 16h Jul 30 20h Jul 31 00h Jul 31 04h Jul 31 08h Jul 31 12h Jul 31 16h Jul 31 20h Aug 1 00h Aug 1 04h Aug 1 08h Aug 1 12h Aug 1 16h Aug 1 20h Aug 2 00h Aug 2 04h Aug 2 08h Aug 2 12h Aug 2 16h Aug 2 20h Aug 3 00h Aug 3 04h Aug 3 08h Aug 3 12h Aug 3 16h Aug 3 20h Aug 4 00h Aug 4 04h Aug 4 08h Aug 4 12h Aug 4 16h Aug 4 20h Aug 5 00h Aug 5 04h Aug 5 08h Aug 5 12h Aug 5 16h Aug 5 20h Aug 6 00h Aug 6 04h Aug 6 08h Aug 6 12h Aug 6 16h Aug 6 20h Aug 7 00h Aug 7 04h Aug 7 08h Aug 7 12h Aug 7 16h Aug 7 20h Aug 8 00h Aug 8 04h Aug 8 08h Aug 8 16h Aug 8 20h Aug 9 00h Aug 9 04h Aug 9 08h Aug 9 12h Aug 9 16h Aug 9 20h Aug 10 00h Aug 10 04h Aug 10 08h Aug 10 12h Aug 10 16h 152 collection intervals · 100% stacked share per window (not lifetime cumulative) ### **Languages** Share of traffic by inferred language - Go**55.4%** - Java**31.1%** - Python**9.9%** - JavaScript**2.9%** - Ruby**0.4%** - C**0.2%** - PHP**0.2%** - .NET**<0.1%** ### **How webhooks travel** Transport security and HTTP method **Transport security** - TLS 1.3**87.7%** - TLS 1.2**12.2%** - Plaintext**0.1%** **HTTP method** - POST**98.3%** - GET**1.7%** - Other**0.0%** ### **Content types** Share of traffic by Content-Type header - application/json**98.9%** - application/x-www-form-urlencoded**0.8%** - text/plain**0.1%** - multipart/form-data**<0.1%** - application/webhook+json**<0.1%** - application/octet-stream**<0.1%** - application/xml**<0.1%** - text/xml**<0.1%** - application/vnd.apache.thrift.json**<0.1%** - application/cloudevents+json**<0.1%** ### **Payload size** Share of traffic by size (from the Content-Length header) - empty**1.8%** - <1 KB**78%** - 1-10 KB**14.7%** - 10-100 KB**5.5%** - 100 KB-1 MB**<0.1%** - 1-10 MB**<0.1%** - >10 MB**<0.1%** ### **Where webhooks come from** Share of traffic by sender country - 🇺🇸 United States**65%** - 🇮🇪 Ireland**15.7%** - 🇬🇧 United Kingdom**5.2%** - 🇳🇱 Netherlands**5.2%** - 🇩🇪 Germany**4.8%** - 🇮🇹 Italy**1.1%** - 🇮🇩 Indonesia**1%** - 🇮🇳 India**1%** - 🇸🇬 Singapore**0.3%** - 🇦🇪 United Arab Emirates**0.1%** Top 10 of 100 countries · drag to spin the globe ### **Where webhooks come from — over time** Top 10 countries + other — each row is one collection interval (adds to 100%) - 🇺🇸 United States - 🇳🇱 Netherlands - 🇮🇪 Ireland - 🇬🇧 United Kingdom - 🇩🇪 Germany - 🇮🇹 Italy - 🇮🇩 Indonesia - 🇮🇳 India - 🇦🇪 United Arab Emirates - 🇸🇬 Singapore - Other Jul 8 05h Jul 8 12h Jul 8 16h Jul 9 00h Jul 9 04h Jul 9 08h Jul 9 12h Jul 9 16h Jul 9 20h Jul 10 00h Jul 10 04h Jul 10 08h Jul 10 12h Jul 10 16h Jul 10 20h Jul 11 00h Jul 11 04h Jul 11 08h Jul 11 12h Jul 11 16h Jul 11 20h Jul 12 00h Jul 12 04h Jul 12 08h Jul 12 12h Jul 12 16h Jul 12 20h Jul 13 00h Jul 13 04h Jul 13 08h Jul 13 12h Jul 13 16h Jul 13 20h Jul 14 00h Jul 14 04h Jul 14 08h Jul 14 12h Jul 19 10h Jul 19 12h Jul 19 16h Jul 19 20h Jul 20 00h Jul 20 04h Jul 20 08h Jul 20 12h Jul 20 16h Jul 20 20h Jul 21 00h Jul 21 04h Jul 21 08h Jul 21 12h Jul 21 16h Jul 21 20h Jul 22 00h Jul 22 04h Jul 22 08h Jul 22 12h Jul 25 20h Jul 26 00h Jul 26 04h Jul 26 08h Jul 26 12h Jul 26 16h Jul 26 20h Jul 27 00h Jul 27 04h Jul 27 08h Jul 27 12h Jul 27 16h Jul 27 20h Jul 28 00h Jul 28 04h Jul 28 08h Jul 28 12h Jul 28 16h Jul 28 20h Jul 29 00h Jul 29 04h Jul 29 08h Jul 29 12h Jul 29 16h Jul 29 20h Jul 30 00h Jul 30 04h Jul 30 08h Jul 30 12h Jul 30 16h Jul 30 20h Jul 31 00h Jul 31 04h Jul 31 08h Jul 31 12h Jul 31 16h Jul 31 20h Aug 1 00h Aug 1 04h Aug 1 08h Aug 1 12h Aug 1 16h Aug 1 20h Aug 2 00h Aug 2 04h Aug 2 08h Aug 2 12h Aug 2 16h Aug 2 20h Aug 3 00h Aug 3 04h Aug 3 08h Aug 3 12h Aug 3 16h Aug 3 20h Aug 4 00h Aug 4 04h Aug 4 08h Aug 4 12h Aug 4 16h Aug 4 20h Aug 5 00h Aug 5 04h Aug 5 08h Aug 5 12h Aug 5 16h Aug 5 20h Aug 6 00h Aug 6 04h Aug 6 08h Aug 6 12h Aug 6 16h Aug 6 20h Aug 7 00h Aug 7 04h Aug 7 08h Aug 7 12h Aug 7 16h Aug 7 20h Aug 8 00h Aug 8 04h Aug 8 08h Aug 8 16h Aug 8 20h Aug 9 00h Aug 9 04h Aug 9 08h Aug 9 12h Aug 9 16h Aug 9 20h Aug 10 00h Aug 10 04h Aug 10 08h Aug 10 12h Aug 10 16h 152 collection intervals · 100% stacked share per window (not lifetime cumulative) ### **About this data** Figures come from two privacy-preserving sources you can switch between above: the [Webhook Relay platform](https://webhookrelay.com/state-of-webhooks/) (real customer webhook delivery) and the public, no-signup [webhook bin](https://webhookrelay.com/state-of-webhooks/webhook-bin/) (an open testing endpoint). Each request is fingerprinted into aggregate counters — provider, client family, language, country, TLS version, HTTP method — and nothing else is kept. ****No payloads, header values, IP addresses or customer identifiers are ever stored, and only shares are shown here.**** Snapshots are collected every few hours and this page rebuilds against the newest one, so it tracks live traffic over time. The over-time charts show one _100% share bar per collection interval_ (roughly every four hours): platform counters are differenced per replica, and bin rollups are differenced across consecutive windows — so a redeploy or window expansion doesn't freeze the mix at lifetime totals. Each row is the top 10 keys plus an _Other_ remainder so it always adds to 100%. The public bin also absorbs a lot of automated and abusive traffic, so its shares skew toward a handful of high-volume senders — switch to _Platform_ for the mix that reflects real customer delivery, and read the rankings as “what we see,” not a market survey. --- --- title: Datadog meta: "og: title": "Forward Webhooks to Any Destination" description: Webhook Relay is a simple, flexible and high performance webhook routing system for internal and public destinations. Transform, route and retry webhooks. url: https://webhookrelay.com/index.md file: /index.md --- # **Forward webhooks anywhere.**Never miss one. Webhook Relay is a [webhook gateway](https://webhookrelay.com/webhook-gateway/) that receives, transforms and delivers webhooks to any destination — a public URL, a private server, or your localhost. With durable retries your events survive outages, deploys and flaky endpoints, so nothing slips through. [Start forwarding free](https://my.webhookrelay.com/register) Free plan · 150 webhooks/month · No credit card required Drop the open-source [**Agent Skills**](https://webhookrelay.com/docs/skills/) into Claude Code or any skill-aware agent — it learns to forward, transform, debug and tunnel webhooks with the relay CLI. $`npx skills add webhookrelay/skills` Prefer a live connection? Wire up the [MCP server](https://webhookrelay.com/docs/mcp/) or read the [agent-ready docs](https://webhookrelay.com/docs/skills.md). ![Customer avatar](https://webhookrelay.com/images/rusenask-avatar.jpeg)![Customer avatar](https://webhookrelay.com/images/luca.png)![Customer avatar](https://webhookrelay.com/images/avatar-03.jpg)![Customer avatar](https://webhookrelay.com/images/avatar-04.jpg)![Customer avatar](https://webhookrelay.com/images/avatar-05.jpg) **40,000+** professionals trust us · used by teams at **500 Fortune** companies Agent-native ## **A dataplane for your AI agents** Webhook Relay is programmable end to end. Your AI agents can define routes, connect third-party services and deploy transformation functions — through the same API, CLI and Skills you use, with every event flowing through one durable hub. 3RD-PARTY SERVICES DESTINATIONS AI AGENTS HUMANS Stripe GitHub Adyen Email AWS GCP Azure JavaScript Jenkins Shopify Slack Discord Email Backend API SQS Datadog Humans Webhook Relay ### Filter, transform, and route Reshape payloads, drop noise, and fan out to the right destinations — with JavaScript or Lua functions your agents can write and deploy. REQUEST FUNCTION · STRIPE → SLACK ``` const payload = JSON.parse(r.body) if (payload.type !== 'invoice.paid') { r.stopForwarding() return } r.setBody(JSON.stringify({ text: \`Paid ${payload.amount}\` })) r.setHeader('Content-Type', 'application/json') ``` [Transform JSON payloads →](https://webhookrelay.com/docs/webhooks/functions/manipulating-json/) ### Queue with rate control Absorb traffic spikes, throttle delivery to protect downstream APIs, and keep a durable queue so nothing is lost during backpressure. DELIVERY RATEcap 10/s ![stripe](https://webhookrelay.com/images/landing/logos/stripe.svg)![github](https://webhookrelay.com/images/landing/logos/github.svg)![slack](https://webhookrelay.com/images/landing/logos/slack.svg) 87654321 ▲ 42/s in18 queued10/s out ![jenkins](https://webhookrelay.com/images/landing/logos/jenkins.svg)![discord](https://webhookrelay.com/images/landing/logos/discord.svg)![email](https://webhookrelay.com/images/landing/logos/email.svg) [Webhook throttling →](https://webhookrelay.com/features/throttling/) ### Detect issues in requests & responses Inspect the payload before delivery and the destination's response after. When JSON is missing or the body is empty, post an alert to any URL. RESPONSE FUNCTION · ALERT ``` if (!r.responseBody) { http.post(cfg.get('alert_webhook_url'), JSON.stringify({ type: 'empty_response', output: r.metadata['output_name'] })) } ``` [Alerting with functions →](https://webhookrelay.com/docs/webhooks/functions/alerting/) [Explore Agent Skills](https://webhookrelay.com/docs/skills/) [Connect via MCP](https://webhookrelay.com/docs/mcp/) Durable delivery ## **Never miss a webhook** A webhook you don't receive might as well have never happened. Every event is saved to durable storage the moment it arrives, then delivered with smart exponential backoff — retrying for up to **30 days** until your endpoint is ready for it. Durable delivery · live convergence 50/ 50 delivered 100%at 95 min 0 25 50 0m 15m 30m 45m 60m 75m 90m handoff 1-hour guarantee DeliveredRetryingAttempts50 webhooks · 275 attempts · 100% by ~73 min - **Seconds** — persistent retries over ~25 minutes for endpoints that only blip. - **Medium** — backoff that ramps up over ~16 hours for outages measured in hours. - **Long** — keeps going for up to 30 days so a destination can come back next week. - **Custom** — set your own delays when you know how your endpoint behaves. [**Explore durable retries **→](https://webhookrelay.com/features/durable-retries/) Any destination ## **Deliver anywhere your services run** Public APIs, services behind your firewall, an app on your laptop — or straight into your cloud on AWS, GCP and Azure. Reach them all without opening ports or changing firewall rules. [

**Public URLs **→

Your API, a partner API, or any SaaS webhook endpoint on the open internet.](https://webhookrelay.com/features/webhook-multiple-destinations/) [

**Private servers **→

Services behind your firewall, reached through the agent — no open ports.](https://webhookrelay.com/features/webhook-to-internal-server/) [

**Localhost **→

Forward live webhooks straight to your laptop while you build and debug.](https://webhookrelay.com/tunnels/) [

**AWS **→

Relay events into S3, SQS or SNS with a cloud service connection.](https://webhookrelay.com/docs/service-connections/aws_sqs/) [

**GCP **→

Publish to Pub/Sub or Cloud Storage in your own GCP project.](https://webhookrelay.com/docs/service-connections/gcp_pubsub/) [

**Azure **→

Cosmos DB & Blob Storage — in progress. Contact us to switch it on.](https://webhookrelay.com/docs/service-connections/azure/) CI/CD & automation ## **Drive your CI/CD from webhooks** Kick off builds, tests and deploys the moment code is pushed or a PR opens — and reach CI servers that sit behind a firewall or have no public IP. Webhook Relay forwards every event straight to your pipeline, so no trigger is ever missed. [

>_** Jenkins **→

Receive GitHub webhooks on Jenkins — even with no public IP.](https://webhookrelay.com/docs/tutorials/cicd/jenkins-plugin/) [

>_** Drone CI **→

Trigger self-hosted Drone.io builds straight from webhooks.](https://webhookrelay.com/blog/using-drone-for-simple-selfhosted-ci-cd/) [

>_** GitHub **→

Forward GitHub push & PR webhooks to localhost or internal CI.](https://webhookrelay.com/blog/receive-github-webhooks-locally/) [

>_** Bitbucket **→

Wire Bitbucket up to Jenkins running behind your firewall.](https://webhookrelay.com/docs/tutorials/cicd/jenkins-bitbucket/) [**Browse all guides **→](https://webhookrelay.com/guides/) How it works ## **Live in under five minutes** 1. 1 ### **Connect a source** Get a stable Webhook Relay endpoint and point any provider — Stripe, GitHub, Shopify, your own app — at it. 2. 2 ### **Add your destinations** Send each event to one or many destinations: public URLs, internal services, or your localhost. Filter, route and transform along the way. 3. 3 ### **We deliver — and keep trying** Every event is stored, delivered, and retried with exponential backoff until it lands. Watch it all in real-time logs. **40k+** developers **30 days** retry window **99.99%** uptime SLA Plays well with everything ## **Works with the tools you already use** Receive webhooks from any provider and deliver them to any service — Webhook Relay sits in the middle and makes sure every event arrives. [stripe webhook guide](https://webhookrelay.com/blog/stripe-webhook-tester/) [github webhook guide](https://webhookrelay.com/blog/github-webhook-tester/) [shopify webhook guide](https://webhookrelay.com/blog/shopify-webhook-tester/) [slack webhook guide](https://webhookrelay.com/blog/slack-webhook-tester/) [twilio webhook guide](https://webhookrelay.com/blog/twilio-webhook-tester/) [jira webhook guide](https://webhookrelay.com/blog/jira-webhooks-guide/) [whatsapp webhook guide](https://webhookrelay.com/blog/whatsapp-cloud-api-webhooks/) [docusign webhook guide](https://webhookrelay.com/blog/docusign-connect-webhooks/) [zendesk webhook guide](https://webhookrelay.com/blog/zendesk-webhooks-guide/) [confluence webhook guide](https://webhookrelay.com/blog/confluence-webhooks/) [splunk webhook guide](https://webhookrelay.com/blog/splunk-webhook-alerts/) [sendgrid webhook guide](https://webhookrelay.com/blog/sendgrid-webhook-tester/) [datadog webhook guide](https://webhookrelay.com/blog/datadog-webhook-tester/) [hubspot webhook guide](https://webhookrelay.com/blog/hubspot-webhook-tester/) [intercom webhook guide](https://webhookrelay.com/blog/intercom-webhook-tester/) [calendly webhook guide](https://webhookrelay.com/blog/calendly-webhook-tester/) [sentry webhook guide](https://webhookrelay.com/blog/sentry-webhook-tester/) [vercel webhook guide](https://webhookrelay.com/blog/vercel-webhook-tester/) [paypal webhook guide](https://webhookrelay.com/blog/paypal-webhook-tester/) [linear webhook guide](https://webhookrelay.com/blog/linear-webhook-tester/) [gitlab webhook guide](https://webhookrelay.com/blog/gitlab-webhook-tester/) [square webhook guide](https://webhookrelay.com/blog/square-webhook-tester/) [clerk webhook guide](https://webhookrelay.com/blog/clerk-webhook-tester/) [razorpay webhook guide](https://webhookrelay.com/blog/razorpay-webhook-tester/) Click any provider for a step-by-step webhook guide, or [browse all guides](https://webhookrelay.com/guides/). Enterprise ## **Enterprise-ready webhook delivery** Webhook Relay is an enterprise-ready webhook delivery platform — SOC 2 Type II certified, encrypted end to end, with SSO, audit logs and role-based access. Or run it entirely on your own infrastructure. [

**SOC 2 Type II **→

Independently audited security controls, reviewed on an ongoing basis.](https://webhookrelay.com/docs/security/) [

**SSO & SAML **→

Single sign-on with Okta, Azure AD and other SAML identity providers.](https://webhookrelay.com/features/sso/) [

**Audit logs & roles **→

A full audit trail and role-based access control for your whole team.](https://webhookrelay.com/features/audit-logs/) [

**Encrypted end to end **→

AES-256 at rest and TLS in transit — or ephemeral buckets that store nothing.](https://webhookrelay.com/docs/security/) [

**99.99% uptime SLA **→

High-availability infrastructure on Google Cloud, with retries for up to 30 days.](https://webhookrelay.com/features/durable-retries/) [

**Self-hosted option **→

Run Webhook Relay on your own servers to keep every payload in-house.](https://webhookrelay.com/contact-sales/) [Talk to sales](https://webhookrelay.com/contact-sales/) [Security overview](https://webhookrelay.com/docs/security/) ## **Your webhooks are global.**Your delivery should be too. Connect a source, pick a destination, and Webhook Relay handles delivery, durable retries and transforms. Set up your first webhook in minutes. [Start for free ->](https://my.webhookrelay.com/register) [ See pricing](https://webhookrelay.com/pricing/) Free plan · No credit card required · 7-day money-back guarantee on paid plans --- --- title: Quora meta: "og: title": "Pricing — Free Plan & Paid from $9.99/mo" description: Webhook Relay pricing: start free, then scale. Plans for webhook forwarding, tunnels, transformations and cloud integrations — from $9.99/mo to Enterprise. url: https://webhookrelay.com/pricing.md file: /pricing.md --- # **Plans that match your needs** No matter how many team members you have - our pricing is simple, transparent and adapts to the size of your company. Start on the free plan · No credit card required · Upgrade or cancel anytime **Free Starter** **$****0.00**/month For individual developers, home automation enthusiasts. - 150 webhooks/month - Secure webhooks - Up to 2 destinations - MCP [Start for free](https://my.webhookrelay.com/register?plan=free-starter&term=yearly&page=plan) No credit card required **Basic** **$****8.99**/month For individual developers, small businesses with small number of events. - 5,000 webhooks/month - Custom subdomains - Whitelabel domains - 10 destinations - 8 bidirectional tunnels - MCP **Extra webhooks ** 0 extra $5/1k [Get started](https://my.webhookrelay.com/register?plan=basic&term=yearly&page=plan) **Business** **$****71.99**/month For larger companies with busy CI/CD pipelines, lots of events. - 60,000 webhooks/month - 50 destinations - Long logs retention - Large webhooks (40MB) - Priority support - MCP **Extra webhooks ** 0 extra $10/10k [Get started](https://my.webhookrelay.com/register?plan=business&term=yearly&page=plan) **Pro** **$****224.99**/month For those going big. - 1M webhooks/month - 200 destinations - Wildcard certificates - Large webhooks (40MB) - Dedicated support - MCP **Extra webhooks ** 0 extra $100/1M [Get started](https://my.webhookrelay.com/register?plan=pro&term=yearly&page=plan) No credit card to start 7-day money-back guarantee Cancel or change plans anytime **Enterprise** For large-scale workloads. [Contact Us](https://webhookrelay.com/pricing/contact-sales) - SAML SSO - SOC2 report - 24×7×365 premium enterprise support - Custom Security Questionnaires - Designated support manager - Uptime SLAs **Quota** Number of webhook requests allowed per month. Each webhook delivery counts towards this quota. Number of bidirectional tunnels available. These allow secure two-way communication between your services. Number of webhook input endpoints you can create. Each input can receive webhooks from different sources. Number of output destinations where webhooks can be forwarded. Each destination can be a different endpoint. **Key Features** All webhook data is encrypted in transit and at rest, ensuring maximum security for your data. Support for webhooks up to 40MB in size, allowing you to send larger payloads when needed. Use your own domain for webhook endpoints, providing a more professional and branded experience. Dedicated static IP address for outgoing webhooks, making it easier to whitelist in your firewall rules. [Multi-factor authentication (MFA)](https://webhookrelay.com/pricing/docs/account/mfa) Protect your account with multi-factor authentication for an extra layer of login security. [Durable webhooks](https://webhookrelay.com/pricing/docs/webhooks/durable-webhooks) Webhooks are persisted and automatically retried until delivery succeeds, so events are never lost. [Outbound throttling](https://webhookrelay.com/pricing/docs/webhooks/outbound-throttling) Control the rate of outgoing webhooks to protect downstream services from being overwhelmed. [Custom TLS version](https://webhookrelay.com/pricing/docs/webhooks/tls-ssl-errors) Set a custom minimum accepted TLS version on your webhook inputs to meet your security requirements. [Legacy TLS versions](https://webhookrelay.com/pricing/docs/webhooks/tls-ssl-errors) Accept legacy TLS versions (down to 1.0) and a wider cipher set on inputs, for old senders that can't use modern TLS. [MCP](https://webhookrelay.com/pricing/docs/mcp) Manage buckets, destinations, functions and webhook replay from Claude, Cursor, Codex and other MCP-compatible agents. **Team** Number of team members who can access and manage your webhook configuration. Support for custom invoicing, purchase orders, and other enterprise billing requirements. **Free Starter** **Quota** 150 Webhooks per month 0 Bidirectional tunnels 1 Inputs 2 Outputs **Features** End-to-end encryption Large webhooks (40MB) Custom domains Static IP Multi-factor authentication (MFA) Durable webhooks Outbound throttling Custom TLS version Legacy TLS versions MCP **Team** 0 Team members Custom invoices, POs, etc. **Basic** **Quota** 5000 Webhooks per month 8 Bidirectional tunnels 10 Inputs 10 Outputs **Features** End-to-end encryption Large webhooks (40MB) Custom domains Static IP Multi-factor authentication (MFA) Durable webhooks Outbound throttling Custom TLS version Legacy TLS versions MCP **Team** 0 Team members Custom invoices, POs, etc. **Business** **Quota** 60000 Webhooks per month 20 Bidirectional tunnels 50 Inputs 50 Outputs **Features** End-to-end encryption Large webhooks (40MB) Custom domains Static IP Multi-factor authentication (MFA) Durable webhooks Outbound throttling Custom TLS version Legacy TLS versions MCP **Team** 5 Team members Custom invoices, POs, etc. **Pro** **Quota** 1000000 Webhooks per month 50 Bidirectional tunnels 200 Inputs 200 Outputs **Features** End-to-end encryption Large webhooks (40MB) Custom domains Static IP Multi-factor authentication (MFA) Durable webhooks Outbound throttling Custom TLS version Legacy TLS versions MCP **Team** 10 Team members Custom invoices, POs, etc. ## **Trusted by 40,000+ developers worldwide** See why teams choose Webhook Relay for their webhook infrastructure Facebook AirBnB Canon Cadbury Quora **"Webhook Relay has become a key piece of our infrastructure. It allowed us to simplify our local development story and enable complex CI pipelines. Good tooling makes your life easier and Webhook Relay nails it." ** ![Luca Antiga](https://webhookrelay.com/pricing/images/luca.png) **Luca Antiga** [CTO at Lightning AI](https://lightning.ai) ![Marcus Chen](https://webhookrelay.com/pricing/images/testimonial-01.jpg) **Marcus Chen** [**@marcusdev**](#0) Webhook Relay has completely transformed our local development workflow. No more ngrok headaches or exposing ports. Our team can now test payment webhooks from Stripe locally in seconds. --- --- title: Webhook Gateway — Durable Webhook Delivery | WebhookRelay meta: "og: title": "Webhook Gateway — Durable Webhook Delivery" description: A webhook gateway authenticates, stores, retries and fans out webhooks. Webhook Relay also delivers to internal and on-prem endpoints with no public IP. url: https://webhookrelay.com/webhook-gateway.md file: /webhook-gateway.md --- # **What Is a Webhook Gateway?** A webhook gateway is the reliability and security layer between the services that send you webhooks and the endpoints that consume them — handling authentication, durable delivery, throttling, fan-out and observability so your application doesn't have to. Webhook Relay is the webhook gateway that also delivers to internal and on-prem services with no public IP. [Start free ->](https://my.webhookrelay.com/register) [See the capabilities](#capabilities) ![Webhook Relay gateway dashboard showing delivery rate, retries and latency across every bucket](https://webhookrelay.com/webhook-gateway/images/account_delivery_health.png) ## **A webhook gateway, defined** Webhooks look simple — one service POSTs to a URL when something happens. In production they're anything but. The sender fires whenever it wants, retries a handful of times (if at all), and gives up. Your endpoint has to be online, fast, authenticated and idempotent every single time, or the event is gone. A **webhook gateway** sits in the middle and takes on that hard part: it accepts every event, verifies it's genuine, stores it durably, and keeps delivering — at a pace your endpoint can handle, to as many destinations as you need, with a full log of what happened. Think of it as an API gateway's counterpart for asynchronous, event-driven traffic. An API gateway fronts the requests _you_ receive and route synchronously; a webhook gateway governs the events _other systems_ push to you, where the hard problems are reliability and delivery rather than routing a live request. ## **Why not just receive webhooks yourself?** You can — until the day you can't. Everything a gateway does is something you'd otherwise build, operate and debug on your own. ### **A deploy drops events** Restart your service for 30 seconds and every webhook that arrives in that window is lost, unless something is holding and retrying them. ### **A spike takes you down** A provider replays a backlog and thousands of events hit at once. Without throttling, the flood, not the traffic, is what breaks you. ### **Signatures aren't verified** Every provider signs differently. Getting HMAC verification right for each one — and rejecting forgeries — is fiddly, security-critical work. ### **Nobody can see what happened** "Did that webhook arrive?" becomes an archaeology project across app logs, instead of one searchable record of every delivery. ### **Build vs. buy** A basic receiver that returns `200` is easy. Making it _reliable_ is where the cost hides — you end up building and operating: - a durable queue that survives restarts, - retry logic with exponential backoff and jitter, - a dead-letter queue and a replay UI, - idempotency and de-duplication, - signature verification for every provider, - metrics, logs and alerting, - and the scaling work when one provider suddenly sends 50× the traffic. That's weeks-to-months of engineering for infrastructure that isn't your product. **Buy** when you want those guarantees out of the box or need to reach private destinations; **build** only if webhook ingestion is itself your core product. ## **What a webhook gateway does** Six capabilities separate a real gateway from a bare endpoint. Webhook Relay ships all of them. [

**Durable delivery**

Every event is persisted the moment it arrives and retried with exponential backoff for up to 30 days — surviving outages, deploys and flaky endpoints.**Durable retries **->](https://webhookrelay.com/webhook-gateway/features/durable-retries/) [

**Authentication**

Verify each provider's signature so you only process genuine events, and forward with the credentials your endpoint expects.**Verify signatures **->](https://webhookrelay.com/webhook-gateway/hmac-verification/) [

**Throttling**

Pace delivery to a rate or concurrency your endpoint can handle, with backpressure — so a burst or a replay never flattens a fragile service.**Throttling **->](https://webhookrelay.com/webhook-gateway/features/throttling/) [

**Deliver to many destinations**

Fan a single webhook out to up to 200 endpoints at once — production, staging, a backup, an analytics sink — configured once and delivered reliably.**Multiple destinations **->](https://webhookrelay.com/webhook-gateway/features/webhook-multiple-destinations/) [

**Delivery to internal servers**

Push events to localhost, on-prem and private endpoints with no public IP, through a lightweight outbound agent. The capability inbound-only gateways lack.**Internal destinations **->](https://webhookrelay.com/webhook-gateway/features/webhook-to-internal-server/) [

**Observability**

Every delivery logged with its request, response, status and latency — searchable, inspectable and one click to retry or replay.**Webhook logs **->](https://webhookrelay.com/webhook-gateway/features/webhook-logs/) ## **The gateway that reaches your private endpoints** Every other webhook gateway assumes your consumer has a public URL. Most real services don't — they run behind a firewall, inside a VPC, on a Kubernetes cluster, or on a laptop during development. Webhook Relay delivers there too. A lightweight agent opens an outbound connection from your network, so events are pushed to **localhost, on-prem servers and internal APIs** without exposing a single inbound port. It's the tunnelling heritage of Webhook Relay combined with a full delivery gateway — a pairing the inbound-only platforms can't match. [Deliver to internal services ->](https://webhookrelay.com/webhook-gateway/features/webhook-to-internal-server/) ![Webhook Relay delivering webhooks to a service behind a firewall with no public IP](https://webhookrelay.com/webhook-gateway/images/landing/webhooks/behind-firewall.png) ## **How Webhook Relay compares** Hookdeck, Convoy and Svix are strong inbound event gateways. Here's where Webhook Relay lines up — and where it's in a category of its own. | **Capability** | **Webhook Relay** | **Hookdeck** | **Convoy** | **Svix** | | --- | --- | --- | --- | --- | | Durable retries | **✓** | **✓** | **✓** | **✓** | | Signature verification | **✓** | **✓** | **✓** | **✓** | | Fan-out to multiple destinations | **✓** | **✓** | **✓** | ~ | | Throttling / rate control | **✓** | **✓** | ~ | ~ | | Payload transformations | **✓** | **✓** | ~ | **✓** | | Delivery logs & inspection | **✓** | **✓** | **✓** | **✓** | | Private / on-prem delivery (no public IP) | **✓** | ✕ | ✕ | ✕ | | Reverse tunnels for localhost | **✓** | ~ | ✕ | ✕ | | Free testing bin, no signup | **✓** | ~ | ✕ | ~ | Summary of each platform's core positioning as of 2026 — see our detailed [Convoy comparison](https://webhookrelay.com/webhook-gateway/blog/convoy-alternative/) for specifics. "Private delivery" means production delivery to endpoints with no public URL, not a dev-only CLI tunnel. ## **Webhook gateway FAQ** ### **What is a webhook gateway?** A webhook gateway is a service that sits between the systems sending you webhooks and the endpoints consuming them. It authenticates incoming events, stores them durably, retries failed deliveries, throttles bursts, fans out to multiple destinations and records every delivery — so your application receives reliable, verified events instead of a raw firehose it has to defend against. ### **Is a webhook gateway the same as an API gateway?** No. An API gateway fronts synchronous requests that clients make to _your_ API — routing, rate-limiting and authenticating live calls. A webhook gateway governs asynchronous events that _other systems_ push to you, where the hard problems are durability and delivery: making sure every event is captured, verified and delivered even when your endpoint is briefly down. ### **Can a webhook gateway deliver to localhost or a private server?** Most cannot — they require your consumer to expose a public URL. Webhook Relay is the exception: a lightweight [agent](https://webhookrelay.com/webhook-gateway/features/webhook-to-internal-server) opens an outbound connection from your network, so webhooks are delivered to localhost, on-prem servers and internal APIs without opening any inbound ports. ### **Should I build a webhook gateway myself or use one?** You can build one, but you're rebuilding a queue, durable storage, a retry scheduler, signature verification per provider, throttling and a delivery dashboard — then operating them. A managed gateway gives you all of that per destination with a switch, which is why most teams reach for one once webhooks become business-critical. ### **Is Webhook Relay a webhook gateway?** Yes. Webhook Relay provides durable delivery, signature verification, throttling, fan-out, transformations and full delivery observability — and uniquely delivers to internal and on-prem endpoints with no public IP. You can [start for free](https://my.webhookrelay.com/register) and test with a no-signup [webhook bin](https://webhookrelay.com/webhook-gateway/webhook-bin). ![Stripes](https://webhookrelay.com/webhook-gateway/images/stripes-dark.svg) ## **Start forwarding webhooks in minutes ** Connect a source, pick a destination, and Webhook Relay handles delivery, retries and transforms. Set up your first webhook in under five minutes. [Start for free ->](https://my.webhookrelay.com/register) Free plan · No credit card required · 7-day money-back guarantee on paid plans --- --- title: Custom Subdomains | WebhookRelay meta: "og: title": "Custom Subdomains" description: Use a custom webhookrelay.com subdomain for your webhook endpoints — get memorable, stable URLs for receiving and forwarding webhooks to any destination. url: https://webhookrelay.com/features/custom-subdomains.md file: /features/custom-subdomains.md --- ![Stripes](https://webhookrelay.com/features/custom-subdomains/images/stripes.svg) FEATURES # **Custom Subdomains** Use a custom webhookrelay.com subdomain for your webhook endpoints — get memorable, stable URLs for receiving and forwarding webhooks to any destination. ![Custom Subdomains](https://webhookrelay.com/features/custom-subdomains/images/features/custom-subdomains/cover.png) Registering your own subdomain (e.g., `your-company.hooks.webhookrelay.com`) is highly recommended. It provides a stable, reusable public URL for your inputs, allowing easy integration with other tools and services. You can point this subdomain to different buckets as needed, offering flexibility and control over your webhook endpoints. ### [Why Use a Custom Subdomain?](#why-use-a-custom-subdomain) When you create a bucket in Webhook Relay, it gets a unique, randomly generated public URL. While functional, these URLs can be long and difficult to remember. If you delete and recreate a bucket, the URL changes, requiring you to update it in all services sending webhooks to that endpoint. Custom subdomains solve this by giving you a persistent, memorable address for your webhooks. ### [Key Benefits:](#key-benefits) - **Stable & Reusable URLs:** Once you set up `your-company.hooks.webhookrelay.com`, this URL remains constant. Even if you change the underlying bucket it points to, your webhook providers won't need to update their configurations. - **Easier to Remember:** A custom subdomain is significantly easier to recall and share than a long, random string of characters. This simplifies debugging and communication with your team. - **Brand Consistency:** Reinforce your brand by using a subdomain that reflects your company or project name. - **Seamless Bucket Switching:** Need to redirect webhooks to a new bucket for testing, development, or a new version of your application? Simply update your custom subdomain's target destination in Webhook Relay. There's no need to change the webhook URL in the source applications. - **Simplified Configuration:** Reduce the hassle of updating webhook URLs across multiple third-party services every time your input destination changes. By using a custom subdomain, you create a more robust, manageable, and professional webhook infrastructure. ![Stripes](https://webhookrelay.com/features/custom-subdomains/images/stripes-dark.svg) ## **Start forwarding webhooks in minutes ** Connect a source, pick a destination, and Webhook Relay handles delivery, retries and transforms. Set up your first webhook in under five minutes. [Start for free ->](https://my.webhookrelay.com/register) Free plan · No credit card required · 7-day money-back guarantee on paid plans --- --- title: Alerts | WebhookRelay meta: "og: title": Alerts description: Define alert policies on your buckets, choose where to get notified (Email, Slack, Discord, Teams, Telegram, Pushover, SMTP or webhook), and Webhook Relay opens and resolves incidents when delivery fails. url: https://webhookrelay.com/features/alerts.md file: /features/alerts.md --- ![Stripes](https://webhookrelay.com/features/alerts/images/stripes.svg) FEATURES # **Alerts** Define alert policies on your buckets, choose where to get notified (Email, Slack, Discord, Teams, Telegram, Pushover, SMTP or webhook), and Webhook Relay opens and resolves incidents when delivery fails. ![Create alert dialog — choose destination, alert policy for delivery failures, and which buckets to monitor](https://webhookrelay.com/features/alerts/images/features/alerts/create-alert.png) Define **alert policies** on the buckets you care about, pick where incidents should go, and Webhook Relay will notify you when a delivery problem starts — and clear it when things recover. ## [How it works](#how-it-works) 1. **Choose a destination** — pick a notification channel and enter its credentials (fields are validated before saving). 2. **Set the policy** — for example _delivery failures_: open an incident on the first failed delivery, or only when the error rate crosses a threshold. 3. **Select buckets** — attach the policy to one or more buckets so only the traffic you care about is watched. When delivery outcomes cross that policy, an **incident opens**. When delivery recovers, the incident **resolves automatically** — no manual cleanup. ## [Where should incidents go?](#where-should-incidents-go) ![Alert destination types: Email, Slack, Discord, Telegram, Microsoft Teams, Pushover, SMTP email, Webhook](https://webhookrelay.com/features/alerts/images/features/alerts/destinations.png) Send incidents to the tools your team already watches. Supported destination types: - **Email** — inbox notifications - **Slack** — channel posts for your on-call or eng channel - **Discord** — server / channel webhooks - **Telegram** — bot messages - **Microsoft Teams** — channel notifications - **Pushover** — mobile push for on-call - **SMTP email** — your own mail server - **Webhook** — POST a structured incident payload to any HTTPS endpoint (PagerDuty, Opsgenie, custom bots, …) You can define **multiple destinations** — for example Slack for the team and a webhook into your incident tool — so the same policy fans out when something breaks. ## [What you get notified about](#what-you-get-notified-about) - **Failed deliveries** — your endpoint returned an error, timed out, or could not be reached - **Error-rate thresholds** — optional: only fire when failures pile up, not on a single blip - **Open and resolved events** — structured payloads for both, so on-call tools can open and close tickets cleanly ## [Incidents](#incidents) When a policy condition is met, Webhook Relay opens an **incident** for that bucket. ![Alerts incidents dashboard with open incident, chart, failure snapshot and open/resolved times](https://webhookrelay.com/features/alerts/images/features/alerts/incidents.jpg) Incidents **close automatically** once delivery recovers (full rolling window without failures and at least one success), or you can **resolve them manually**. Open and past incidents stay on the Alerts page — filter Open / Resolved / All and revisit failure snapshots anytime. Setup details: [Alerts docs](https://webhookrelay.com/features/alerts/docs/webhooks/alerts/). ## [Why use alerts](#why-use-alerts) - **Know before customers do** — failures show up in Slack or your pager, not in a support ticket hours later - **Less noise than DIY scripts** — policies and auto-resolve replace one-off cron checks and response-function hacks - **Scoped to what matters** — attach policies to specific buckets instead of alerting on the whole account - **Pairs with observability** — dig into the exact failure with [webhook logs & monitoring](https://webhookrelay.com/features/alerts/features/webhook-logs/), then replay with [durable retries](https://webhookrelay.com/features/alerts/features/durable-retries/) For step-by-step setup, see the [Alerts docs](https://webhookrelay.com/features/alerts/docs/webhooks/alerts/). For one-off custom logic (e.g. alert only when a payload field is wrong), use [alerting from functions](https://webhookrelay.com/features/alerts/docs/webhooks/functions/alerting/). Built-in Alerts cover the common case: delivery health, without writing code. [Create a free account](https://my.webhookrelay.com/register) and add your first alert policy — or [talk to sales](https://webhookrelay.com/features/alerts/contact-sales/) if you need help wiring notifications into your on-call stack. ![Stripes](https://webhookrelay.com/features/alerts/images/stripes-dark.svg) ## **Start forwarding webhooks in minutes ** Connect a source, pick a destination, and Webhook Relay handles delivery, retries and transforms. Set up your first webhook in under five minutes. [Start for free ->](https://my.webhookrelay.com/register) Free plan · No credit card required · 7-day money-back guarantee on paid plans --- --- title: Static Outgoing IP Address | WebhookRelay meta: "og: title": "Static Outgoing IP Address" description: How to use a static outgoing IP address for your webhooks to unlock integrations that require whitelisting your IP address url: https://webhookrelay.com/features/static-outgoing-ip.md file: /features/static-outgoing-ip.md --- ![Stripes](https://webhookrelay.com/features/static-outgoing-ip/images/stripes.svg) FEATURES # **Static Outgoing IP Address** How to use a static outgoing IP address for your webhooks to unlock integrations that require whitelisting your IP address ![Static outgoing IP configuration](https://webhookrelay.com/features/static-outgoing-ip/images/features/static-ip/cover.png) ## [The Challenge: IP Whitelisting Requirements](#the-challenge-ip-whitelisting-requirements) Many external systems, APIs, and services enhance security by implementing IP address whitelisting. This means they only accept incoming requests from a pre-approved list of IP addresses. If you're forwarding webhooks or connecting services through dynamic or unpredictable IP addresses, integrating with these systems becomes a significant hurdle. Your requests might be blocked simply because the source IP isn't recognized or whitelisted. Keywords: IP whitelisting, firewall rules, secure integration, third-party API, blocked requests, source IP filtering. ## [The Solution: Webhook Relay's Static Outgoing IP](#the-solution-webhook-relays-static-outgoing-ip) Webhook Relay simplifies this process by providing a **dedicated static outgoing IP address** for all webhooks and requests forwarded through your tunnels. When you use Webhook Relay to send webhooks to an external service, the request originates from our predictable, static IP address. ### [How it Works:](#how-it-works) 1. Your service sends a webhook to your Webhook Relay public endpoint. 2. Webhook Relay receives the webhook and forwards it to your configured destination(s). 3. Crucially, the forwarded webhook originates from Webhook Relay's static IP address, not your original server's IP. ### [Getting the Static IP:](#getting-the-static-ip) You can find the current static outgoing IP address within your [bucket details page](https://my.webhookrelay.com/buckets) in the dashboard. ### [Benefits:](#benefits) - **Effortless Integration:** Easily connect with any service requiring IP whitelisting. Simply add the Webhook Relay static IP to their allowlist. - **Enhanced Security:** Maintain security protocols without complex network configurations on your end. - **Reliability:** Ensure consistent delivery as your source IP remains constant, preventing disruptions due to IP changes. - **Simplified Setup:** No need for dedicated infrastructure or complex workarounds to achieve a static IP. By leveraging Webhook Relay's static outgoing IP, you can bypass the common integration challenges posed by IP whitelisting, ensuring your webhooks are reliably delivered to secure external systems. ![Stripes](https://webhookrelay.com/features/static-outgoing-ip/images/stripes-dark.svg) ## **Start forwarding webhooks in minutes ** Connect a source, pick a destination, and Webhook Relay handles delivery, retries and transforms. Set up your first webhook in under five minutes. [Start for free ->](https://my.webhookrelay.com/register) Free plan · No credit card required · 7-day money-back guarantee on paid plans --- --- title: Forward Webhooks to Multiple Destinations | WebhookRelay meta: "og: title": "Forward Webhooks to Multiple Destinations" description: Fan out a single webhook to multiple destinations with Webhook Relay — ideal for data replication, backups and sending events to several services at once. url: https://webhookrelay.com/features/webhook-multiple-destinations.md file: /features/webhook-multiple-destinations.md --- ![Stripes](https://webhookrelay.com/features/webhook-multiple-destinations/images/stripes.svg) FEATURES # **Forward Webhooks to Multiple Destinations** Fan out a single webhook to multiple destinations with Webhook Relay — ideal for data replication, backups and sending events to several services at once. ![Forward webhooks to multiple destinations](https://webhookrelay.com/features/webhook-multiple-destinations/images/features/multiple-destinations/cover.png) Once the webhooks bucket is created, add up to 200 destinations that you want to forward the webhooks to! ## [Destination types for notifications](#destination-types-for-notifications) When you need people (not only services) to receive events — for example [delivery alerts and incidents](https://webhookrelay.com/features/webhook-multiple-destinations/features/alerts/) — Webhook Relay also supports first-class notification channels: ![Destination types: Email, Slack, Discord, Telegram, Microsoft Teams, Pushover, SMTP email, Webhook](https://webhookrelay.com/features/webhook-multiple-destinations/images/features/multiple-destinations/destinations.png) - **Email** and **SMTP email** - **Slack**, **Discord**, **Telegram**, **Microsoft Teams** - **Pushover** for mobile push - **Webhook** for any HTTPS endpoint Fan out the same event to several of these at once, or mix notification channels with your application HTTP endpoints. ## [Why Forward Webhooks to Multiple Destinations?](#why-forward-webhooks-to-multiple-destinations) In today's interconnected digital landscape, data often needs to flow to various systems simultaneously. Manually managing this distribution can be complex and error-prone. Webhook Relay's multi-destination forwarding simplifies this process, ensuring your webhook data reaches all necessary endpoints reliably and efficiently. ### [Key Use Cases & Benefits:](#key-use-cases-benefits) - **Data Replication & Backup:** Send incoming webhooks to both your primary processing service and a backup or logging system (like an S3 bucket or a log aggregator) for redundancy and disaster recovery. - **Parallel Processing:** Distribute webhooks to different microservices that handle distinct aspects of the event. For example, one service might update a database, while another triggers a notification, and a third feeds data into an analytics platform. - **System Integration:** Seamlessly integrate multiple third-party or internal applications that need to react to the same event. Forward a payment confirmation webhook to your CRM, accounting software, and fulfillment system all at once. - **Development & Testing:** Route webhooks to your production environment, staging server, and local development machine concurrently. This allows for safe testing and debugging without disrupting live services. - **Fan-Out Architectures:** Implement fan-out patterns where a single event triggers multiple downstream actions across different systems or components. With Webhook Relay, you configure your destinations once, and we handle the reliable delivery to each endpoint, simplifying your architecture and reducing operational overhead. Fan-out is one capability of Webhook Relay's [webhook gateway](https://webhookrelay.com/features/webhook-multiple-destinations/webhook-gateway/) — combine it with [durable retries](https://webhookrelay.com/features/webhook-multiple-destinations/features/durable-retries/), [throttling](https://webhookrelay.com/features/webhook-multiple-destinations/features/throttling/) and [delivery logs](https://webhookrelay.com/features/webhook-multiple-destinations/features/webhook-logs/) to run production webhook infrastructure without building it yourself. ![Stripes](https://webhookrelay.com/features/webhook-multiple-destinations/images/stripes-dark.svg) ## **Start forwarding webhooks in minutes ** Connect a source, pick a destination, and Webhook Relay handles delivery, retries and transforms. Set up your first webhook in under five minutes. [Start for free ->](https://my.webhookrelay.com/register) Free plan · No credit card required · 7-day money-back guarantee on paid plans --- --- title: Getting Started | WebhookRelay meta: "og: title": "Getting Started" description: What is Webhook Relay and how you can use it. url: https://webhookrelay.com/docs.md file: /docs.md --- ![Stripes](https://webhookrelay.com/docs/images/stripes.svg) Documentation **Fundamentals** # **Getting Started** What is Webhook Relay and how you can use it. ## [What is Webhook Relay?](#what-is-webhook-relay) Webhook Relay is an **enterprise-ready webhook delivery platform** — SOC 2 Type II certified, encrypted at rest and in transit, with SSO, audit logs, role-based access and an optional self-hosted deployment (see [Enterprise-ready](#enterprise-ready) below). It is a secure tunneling solution that provides: - [Webhook Forwarding](#Webhook-Forwarding) - secure, unidirectional by default with optional request/response transformation. - [Bidirectional Tunnelling](#Tunnels) - fast tunnels for direct access to any HTTP service. This is ideal for accessing internal APIs and demoing websites. Check out our dashboard's [home page](https://my.webhookrelay.com/) to get started quickly. ## [Use Cases](#use-cases) Popular use cases: - Receive Stripe, Shopify, GitHub and other webhooks on your local machine while developing your application. - Allow remote clients to connect to your backend, for example AWS EC2 instances communicating with your MacBook Pro. - Forward a single webhook to multiple destinations, for example Slack and GCP PubSub. - Transform webhooks, for example Docker push webhook into a Slack message. - Demo your website to clients or customers. - Access IoT devices and sensors remotely. ## [Enterprise-ready](#enterprise-ready) Webhook Relay is built for teams that can't afford to miss an event: - **SOC 2 Type II** certified — read the [security & tech overview](https://webhookrelay.com/docs/docs/security/). - **Encryption everywhere** — AES-256 at rest, TLS in transit, plus optional ephemeral buckets that store nothing. - **SSO & SAML** with [Okta, Azure AD and more](https://webhookrelay.com/docs/features/sso/), [role-based access](https://webhookrelay.com/docs/features/team-member-roles/) and [audit logs](https://webhookrelay.com/docs/features/audit-logs/). - **99.99% uptime SLA** on high-availability infrastructure, with durable retries for up to 30 days. - **Self-hosted option** — run Webhook Relay on your own servers to keep every payload in-house. [Talk to sales](https://webhookrelay.com/docs/contact-sales/). ## [Webhook Forwarding](#webhook-forwarding) Webhook forwarding is by default a uni-directional ("one way") way to send webhooks to other internal and public destinations. ![Webhook Relay forwarding](https://webhookrelay.com/docs/images/generic/forwarding.png) Key facts: - Unidirectional by default, but you can enable it to wait for the response. - Single received webhook can be forwarded to multiple destinations. - You can use **Functions** to execute custom code when a webhook is received, for example to filter or modify requests. Use webhook forwarding when: - Your requirement is primarily "_fire and forget_". - You need to forward webhooks to internal destinations ([quick start](https://my.webhookrelay.com/new-internal-destination)). - You need to forward a single webhook to multiple destinations ([quick start](https://my.webhookrelay.com/new-public-destination)). - You need to transform webhook, for example Docker push webhook into a Slack message ([see examples](https://webhookrelay.com/docs/docs/webhooks/functions/)). ## [Inputs](#inputs) **Inputs** represent your public endpoints. These are the unique [URLs](https://en.wikipedia.org/wiki/URL) for instance, `https://xyz.hooks.webhookrelay.com`. Each **input** have several configuration options: - Whitelabel domain name (configurable in all paid plans). - Static response code, headers and body. - Dynamic response coming from any destination (must respond within 10 seconds). - Each input can have an attached Function ([examples](https://webhookrelay.com/docs/docs/webhooks/functions/)) which can modify response and request. ## [Outputs](#outputs) **Outputs** define your destination, where webhooks will be sent. These destinations can either be an HTTP server running on your local machine ([http://localhost:8080](http://localhost:8080)) or a public server such as [https://example.com](https://example.com). Main options for output configuration: - Destination (where to forward). - Path lock - whether to dynamically append URL paths (_xyz.hooks.webhookrelay.com/foo/bar_ -> _localhost:8080/foo/bar_) - Attached [function](https://webhookrelay.com/docs/docs/webhooks/functions/) to execute on a webhook. ## [Buckets](#buckets) [Buckets](https://my.webhookrelay.com/buckets) are a grouping mechanism in Webhook Relay for **Inputs** and **Outputs**. You can also enable certain settings such as authentication and large webhook support (requests up to 50MB). ## [Tunnels](#tunnels) Tunnels fully expose your local HTTP services to the internet. This is ideal for accessing internal APIs and demoing websites: ![Webhook Relay tunnels](https://webhookrelay.com/docs/images/generic/tunnels.png) You can create, view and manage your tunnels here: [https://my.webhookrelay.com/tunnels](https://my.webhookrelay.com/tunnels) or check out the [quick start](https://my.webhookrelay.com/new-tunnel). ## [Access tokens](#access-tokens) Access tokens are used to authenticate Webhook Relay agents and any other API requests. You can provision a key and secret pair here: [https://my.webhookrelay.com/tokens](https://my.webhookrelay.com/tokens). Did this page help you? --- --- title: Forwarding Rules - Filter and Route Webhooks | WebhookRelay meta: "og: title": "Forwarding Rules - Filter and Route Webhooks" description: Filter and route webhooks on request body, JSON paths, query parameters, URL path and IP address. Control which webhooks reach each destination. url: https://webhookrelay.com/features/forwarding-rules.md file: /features/forwarding-rules.md --- ![Stripes](https://webhookrelay.com/features/forwarding-rules/images/stripes.svg) FEATURES # **Forwarding Rules - Filter and Route Webhooks** Filter and route webhooks on request body, JSON paths, query parameters, URL path and IP address. Control which webhooks reach each destination. ![Forwarding Rules](https://webhookrelay.com/features/forwarding-rules/images/features/forwarding-rules/cover.png) Take full control over your webhook routing with powerful forwarding rules. Filter incoming webhooks based on multiple criteria and ensure only the relevant requests reach each destination. ## [Supported Filtering Options](#supported-filtering-options) Webhook Relay forwarding rules support filtering by: - **Request Body** - Match webhooks based on the raw request body content - **JSON Paths** - Use JSON path expressions to filter based on specific fields in JSON payloads - **URL Query Parameters** - Filter based on query string parameters in the webhook URL - **URL Path** - Route webhooks based on the URL path patterns - **IP Address** - Allow or block webhooks from specific IP addresses or ranges ## [How to Configure Forwarding Rules](#how-to-configure-forwarding-rules) Forwarding rules can be configured in the Webhook Relay dashboard: 1. Navigate to your **Bucket Details** 2. Select the **Output** you want to configure 3. Click on **Rules** to add or manage forwarding rules Each output destination can have its own set of rules, allowing you to route different webhooks to different endpoints based on their content or source. ## [Use Cases](#use-cases) ### [Filter by Event Type](#filter-by-event-type) Use JSON path filtering to route webhooks based on event types. For example, send only `order.completed` events to your fulfillment service while `payment.failed` events go to your support system. ### [Security Filtering](#security-filtering) Restrict webhook processing to known IP addresses, ensuring only trusted sources can trigger your workflows. ### [Environment Routing](#environment-routing) Use URL path or query parameter filtering to route webhooks to different environments (staging, production) based on the incoming request. ### [Selective Processing](#selective-processing) Combine multiple rules to create sophisticated routing logic. For example, only forward webhooks that contain a specific customer ID in the JSON payload and come from an allowed IP range. ## [Benefits](#benefits) - **Reduce Noise** - Only forward webhooks that matter to each destination - **Improve Security** - Block unwanted or suspicious requests at the relay level - **Simplify Architecture** - Handle routing logic in Webhook Relay instead of your application code - **Save Resources** - Avoid processing irrelevant webhooks in your backend services ![Stripes](https://webhookrelay.com/features/forwarding-rules/images/stripes-dark.svg) ## **Start forwarding webhooks in minutes ** Connect a source, pick a destination, and Webhook Relay handles delivery, retries and transforms. Set up your first webhook in under five minutes. [Start for free ->](https://my.webhookrelay.com/register) Free plan · No credit card required · 7-day money-back guarantee on paid plans --- --- title: Webhook Logs & Delivery Monitoring | WebhookRelay meta: "og: title": "Webhook Logs & Delivery Monitoring" description: See every webhook delivery, latency and failure in one place. Webhook Relay logs each event with its status and response, and lets you retry any delivery. url: https://webhookrelay.com/features/webhook-logs.md file: /features/webhook-logs.md --- ![Stripes](https://webhookrelay.com/features/webhook-logs/images/stripes.svg) FEATURES # **Webhook Logs & Delivery Monitoring** See every webhook delivery, latency and failure in one place. Webhook Relay logs each event with its status and response, and lets you retry any delivery. ![Account-wide webhook delivery health — requests, delivery rate, retries and latency per bucket](https://webhookrelay.com/features/webhook-logs/images/account_delivery_health.png) **You can't fix what you can't see.** Most webhook setups are a black box: an event either lands or it doesn't, and when something breaks you're left grepping application logs hoping the payload was written down somewhere. Webhook Relay records every delivery — request, response, status, latency and retry history — so the answer to "did that webhook arrive, and if not, why?" is always one click away. ## [The problem: webhooks fail silently](#the-problem-webhooks-fail-silently) A webhook that never arrives leaves no trace on your side. The sender thinks it delivered, your service never saw it, and nobody notices until a customer asks where their order confirmation went. Even when you do log incoming requests, you're missing half the story — the response your endpoint returned, how long it took, how many times it was retried, and whether the failure was a one-off blip or a bucket that's been failing all week. Building that visibility yourself means shipping structured logs, a metrics pipeline, dashboards and alerting — a project in its own right, just to answer basic questions about your own traffic. ## [The solution: every delivery, logged and searchable](#the-solution-every-delivery-logged-and-searchable) Webhook Relay captures every request the moment it arrives and every delivery attempt that follows. Nothing is a black box: - **What was sent** — method, path, headers and full body. - **What came back** — your endpoint's status code, response headers and body. - **How it went** — delivered, retrying or failed, with latency and the number of attempts. ### [Account-wide delivery health](#account-wide-delivery-health) The **Usage** view rolls every bucket up into one picture: total requests, deliveries, overall success rate, and a per-bucket breakdown of requests, success rate, failures, retries and average latency. Buckets are ranked by failures, so the ones that need attention surface at the top — and a **failures-over-time** sparkline shows whether a problem is new or ongoing. Switch the window between 7 days, 30 days, 6 months and 12 months. ### [Per-bucket monitoring](#per-bucket-monitoring) ![Per-bucket delivery rate, average latency and the full request log with status and response codes](https://webhookrelay.com/features/webhook-logs/images/bucket_health.png) Open any bucket to see its delivery rate, average latency (including the peak day) and a live log of every request. Filter by time range, delivery status or output destination to zero in on exactly the deliveries you care about — for example, every failed delivery to your production endpoint in the last 24 hours. ### [Inspect any delivery](#inspect-any-delivery) ![Full request and response detail for a single webhook delivery, including headers and body](https://webhookrelay.com/features/webhook-logs/images/webhook_details.png) Click any request to see the complete picture: the exact payload that came in, every header, and the full response your endpoint returned. It's the fastest way to debug a signature mismatch, a malformed body or an endpoint that's quietly returning a `500` — no need to reproduce anything or add logging to your own service. ### [Retry, recover and replay](#retry-recover-and-replay) Observability isn't just for looking — it's for fixing. From the log you can **resend** any delivery, **recover failed** deliveries in bulk after an endpoint recovers, or **replay missing** events to backfill a destination that was down. Combined with [durable retries](https://webhookrelay.com/features/webhook-logs/features/durable-retries/), a delivery that failed while your service was deploying is already being retried automatically — and if you ever need to force one through by hand, it's one click. ## [Where it earns its keep](#where-it-earns-its-keep) - **Debugging integrations.** See the raw payload a provider actually sent, and the exact response you returned, without touching your own code. - **Catching silent failures.** A bucket dropping from 99% to 60% success rate is obvious at a glance, long before a customer complains. Prefer a push? Wire [alerts](https://webhookrelay.com/features/webhook-logs/features/alerts/) so incidents open when deliveries fail. - **Incident recovery.** After an outage, recover and replay everything that failed instead of reconciling by hand. - **Capacity and latency.** Watch average and peak latency per bucket to spot a slow endpoint before it becomes a backlog. ## [Part of a complete webhook gateway](#part-of-a-complete-webhook-gateway) Delivery logs are the observability layer of Webhook Relay's [webhook gateway](https://webhookrelay.com/features/webhook-logs/webhook-gateway/) — the same platform that gives you [durable retries](https://webhookrelay.com/features/webhook-logs/features/durable-retries/), [throttling](https://webhookrelay.com/features/webhook-logs/features/throttling/), [fan-out to multiple destinations](https://webhookrelay.com/features/webhook-logs/features/webhook-multiple-destinations/) and [delivery to internal services](https://webhookrelay.com/features/webhook-logs/features/webhook-to-internal-server/). For a tamper-evident record of _configuration_ changes — who edited a bucket, output or forwarding rule — see [audit logs](https://webhookrelay.com/features/webhook-logs/features/audit-logs/). Ready to stop guessing whether your webhooks arrived? [Create a free account](https://my.webhookrelay.com/register) and watch your first delivery land in real time. ![Stripes](https://webhookrelay.com/features/webhook-logs/images/stripes-dark.svg) ## **Start forwarding webhooks in minutes ** Connect a source, pick a destination, and Webhook Relay handles delivery, retries and transforms. Set up your first webhook in under five minutes. [Start for free ->](https://my.webhookrelay.com/register) Free plan · No credit card required · 7-day money-back guarantee on paid plans --- --- title: Durable Webhook Retries | WebhookRelay meta: "og: title": "Durable Webhook Retries" description: Never lose a webhook to a flaky endpoint. Webhook Relay retries failed deliveries for up to 30 days with exponential backoff, surviving outages. url: https://webhookrelay.com/features/durable-retries.md file: /features/durable-retries.md --- ![Stripes](https://webhookrelay.com/features/durable-retries/images/stripes.svg) FEATURES # **Durable Webhook Retries** Never lose a webhook to a flaky endpoint. Webhook Relay retries failed deliveries for up to 30 days with exponential backoff, surviving outages. ![Durable webhook retries](https://webhookrelay.com/features/durable-retries/images/features/durable-retries/cover.png) **A webhook you don't receive might as well have never happened.** Durable retries make sure that never happens — Webhook Relay holds on to every event and keeps trying until your endpoint is ready for it. Here's durability in one picture: 50 webhooks to an endpoint that fails ~80% of the time, all converging on **delivered** over time. Press **play** (or scrub the timeline), and switch to **Retry attempts** to see the retry effort behind the curve. Durable delivery · live convergence 50/ 50 delivered 100%at 95 min 0 25 50 0m 15m 30m 45m 60m 75m 90m handoff 1-hour guarantee DeliveredRetryingAttempts50 webhooks · 275 attempts · 100% by ~73 min ## [The problem: a failed delivery is a lost event](#the-problem-a-failed-delivery-is-a-lost-event) Webhooks arrive whenever the sender feels like it — and that's rarely when your server is at its best. A deploy, a restart, a database hiccup, a five-minute outage, a downstream API having a bad afternoon. Miss the delivery and the event is usually gone for good. Most providers retry a handful of times over a few minutes, then give up. Then come the questions nobody enjoys. Which payment confirmations did we drop? Why didn't that build trigger? Where did that order go? Reconciling missing webhooks by hand is slow, and some events you simply can't get back. ## [The solution: deliveries that don't give up](#the-solution-deliveries-that-dont-give-up) Switch on durable retries for any destination and Webhook Relay takes responsibility for getting the event there. Every webhook is saved to durable storage the moment it arrives, so it survives restarts, crashes and deploys on both ends. If the first attempts fail, delivery doesn't stop — it backs off and keeps trying on a schedule you choose, all the way out to 30 days. ![Retries with exponential backoff over time](https://webhookrelay.com/features/durable-retries/images/features/durable-retries/retry-schedule.png) ### [Pick how patient you want to be](#pick-how-patient-you-want-to-be) Choose a retry schedule per destination: - **Seconds** — quick, persistent retries over about 25 minutes. Ideal for endpoints that only ever blip. - **Medium** — retries that ramp up over roughly 16 hours, for outages measured in hours. - **Long** — keeps going for up to 30 days, so a destination can come back next week and still get its events. - **Custom** — set your own delays when you know exactly how your endpoint behaves. Each retry waits a little longer than the last — exponential backoff — so a struggling server gets room to recover instead of being hammered while it's already down. ### [How it works](#how-it-works) 1. **Every event is saved first.** The moment a webhook reaches Webhook Relay it's persisted, before any delivery is attempted. 2. **Fast retries happen right away.** Transient blips are usually cleared within the first few attempts, with no delay you'd notice. 3. **Stubborn failures go durable.** If a destination is still failing after about 15 minutes, delivery is handed to the durable retry engine, which works through your schedule until the event lands or the deadline passes. You can watch it happen. Stalled deliveries show up clearly in your logs with their next retry time, so you always know what's in flight and what's waiting. ## [Works everywhere your webhooks go](#works-everywhere-your-webhooks-go) Durable retries cover both kinds of destination: - **Public HTTPS endpoints** — your API, a partner's API, any SaaS webhook URL. - **Internal destinations** — services behind your firewall reached through the [Webhook Relay agent](https://webhookrelay.com/features/durable-retries/features/webhook-to-internal-server/), even ones that were offline when the event arrived. ## [Where it earns its keep](#where-it-earns-its-keep) - **Payments and billing.** A processor or your own billing service goes down for an hour. The confirmations wait, then deliver — no manual reconciliation, no lost revenue events. - **Deploys without dropped events.** Ship in the middle of the day. Webhooks that arrive during the restart queue up and land the moment your service is healthy again. - **Flaky partner endpoints.** Integrating with a service that returns 500s under load? Let it fail and recover on its own schedule instead of writing your own retry plumbing. - **CI/CD and automation.** A webhook that kicks off a build or a job is too important to lose to a momentary network glitch. ## [See it in action](#see-it-in-action) [**flakey-script**](https://github.com/webhookrelay/flakey-script) is a small open-source receiver that fails on purpose ~80% of the time, then always succeeds once an event is an hour old. Point a bucket at it with durable retries on and watch every webhook retry and converge on delivered — read the [launch walkthrough](https://webhookrelay.com/features/durable-retries/blog/durable-webhook-retries-demo/) or follow the step-by-step [durable webhooks guide](https://webhookrelay.com/features/durable-retries/docs/webhooks/durable-webhooks/). ## [Stop building retry queues by hand](#stop-building-retry-queues-by-hand) The durability you'd normally bolt together from a queue, a database and a cron job — done for you, per destination, with a single switch. Pair it with [throttling](https://webhookrelay.com/features/durable-retries/features/throttling/) to control exactly how fast those retries reach a recovering server. Durable delivery is one layer of Webhook Relay's [webhook gateway](https://webhookrelay.com/features/durable-retries/webhook-gateway/) — see how it fits alongside authentication, fan-out and observability. Want the details? The [durable webhooks documentation](https://webhookrelay.com/features/durable-retries/docs/webhooks/durable-webhooks/) covers retry schedules, exponential backoff, idempotency and how to design a receiver that handles retries safely. Ready to make sure no webhook slips through? [Create a free account](https://my.webhookrelay.com/register) and turn on durable retries for your first destination. ![Stripes](https://webhookrelay.com/features/durable-retries/images/stripes-dark.svg) ## **Start forwarding webhooks in minutes ** Connect a source, pick a destination, and Webhook Relay handles delivery, retries and transforms. Set up your first webhook in under five minutes. [Start for free ->](https://my.webhookrelay.com/register) Free plan · No credit card required · 7-day money-back guarantee on paid plans --- --- title: Webhooks to Internal Servers | WebhookRelay meta: "og: title": "Webhooks to Internal Servers" description: Forward public webhooks to internal servers behind a firewall or NAT with Webhook Relay — no public IP, port forwarding or router changes required. url: https://webhookrelay.com/features/webhook-to-internal-server.md file: /features/webhook-to-internal-server.md --- ![Stripes](https://webhookrelay.com/features/webhook-to-internal-server/images/stripes.svg) FEATURES # **Webhooks to Internal Servers** Forward public webhooks to internal servers behind a firewall or NAT with Webhook Relay — no public IP, port forwarding or router changes required. Webhook Relay provides a secure and reliable way to receive webhooks from external services directly on your local machine or servers located within your private network (on-prem). This eliminates the need for complex firewall configurations or exposing your internal endpoints directly to the internet. ## [How it Works](#how-it-works) 1. Install the Webhook Relay agent on your target machine (local PC or internal server). 2. Start the agent with `relay forward -b example http://localhost:4500/webhooks` ## [Key Benefits](#key-benefits) - **Bypass Firewalls and NATs:** Receive webhooks without needing public IP addresses or opening firewall ports. Webhook Relay establishes an outbound connection from your network, which is typically allowed. - **Enhanced Security:** Your internal endpoints remain hidden and protected from direct internet exposure. Communication happens over a secure, encrypted tunnel. - **Simplified Development & Testing:** Easily test webhook integrations during development by pointing webhooks directly to your local development environment. - **Connect On-Prem Systems:** Integrate cloud services with internal applications running on private networks seamlessly. - **Reliability:** The agent ensures persistent connectivity and reliable delivery of webhooks. Delivering to private endpoints is what sets Webhook Relay apart as a [webhook gateway](https://webhookrelay.com/features/webhook-to-internal-server/webhook-gateway/) — the same platform adds [durable retries](https://webhookrelay.com/features/webhook-to-internal-server/features/durable-retries/), [throttling](https://webhookrelay.com/features/webhook-to-internal-server/features/throttling/), [fan-out](https://webhookrelay.com/features/webhook-to-internal-server/features/webhook-multiple-destinations/) and [delivery logs](https://webhookrelay.com/features/webhook-to-internal-server/features/webhook-logs/) on top of the tunnel. ![Stripes](https://webhookrelay.com/features/webhook-to-internal-server/images/stripes-dark.svg) ## **Start forwarding webhooks in minutes ** Connect a source, pick a destination, and Webhook Relay handles delivery, retries and transforms. Set up your first webhook in under five minutes. [Start for free ->](https://my.webhookrelay.com/register) Free plan · No credit card required · 7-day money-back guarantee on paid plans --- --- title: Tutorials | WebhookRelay meta: "og: title": Tutorials description: Step-by-step Webhook Relay tutorials covering CI/CD, edge and IoT devices, payload transformations and streaming webhooks into data warehouses. url: https://webhookrelay.com/docs/tutorials.md file: /docs/tutorials.md --- ![Stripes](https://webhookrelay.com/docs/tutorials/images/stripes.svg) Documentation **Fundamentals** # **Tutorials** Step-by-step Webhook Relay tutorials covering CI/CD, edge and IoT devices, payload transformations and streaming webhooks into data warehouses. Browse our hands-on tutorials to learn how to receive, route and transform webhooks with Webhook Relay across a range of real-world use cases. ## [n8n](#n8n) Receive webhooks and inbound email in self-hosted n8n with no public IP, port forwarding or tunnel — n8n connects out over a WebSocket and is never exposed: - [Webhook Trigger (self-hosted / on-prem)](https://webhookrelay.com/docs/tutorials/docs/tutorials/n8n/webhook-trigger/) — receive HTTP webhooks in n8n behind a firewall or NAT - [Email Trigger](https://webhookrelay.com/docs/tutorials/docs/tutorials/n8n/email-trigger/) — start workflows from inbound email, no IMAP or mail server - [WhatsApp Cloud API webhook setup](https://webhookrelay.com/docs/tutorials/docs/tutorials/n8n/whatsapp-cloud-api-webhook/) — receive WhatsApp messages in n8n, verify-token handshake handled automatically ## [Email](#email) Receive inbound email as a webhook and forward it anywhere (see [Email to Webhook](https://webhookrelay.com/docs/tutorials/email-to-webhook/)): - [Email to Slack](https://webhookrelay.com/docs/tutorials/docs/tutorials/email/slack/) — post inbound email into a Slack channel - [Email to Discord](https://webhookrelay.com/docs/tutorials/docs/tutorials/email/discord/) — route email to a Discord channel - [Email to Microsoft Teams](https://webhookrelay.com/docs/tutorials/docs/tutorials/email/microsoft-teams/) — post a Teams card per email - [Email to Google Sheets](https://webhookrelay.com/docs/tutorials/docs/tutorials/email/google-sheets/) — append a row per email - [Email to Airtable](https://webhookrelay.com/docs/tutorials/docs/tutorials/email/airtable/) — create an Airtable record - [Email to Notion](https://webhookrelay.com/docs/tutorials/docs/tutorials/email/notion/) — create a page in a Notion database - [Email to your API](https://webhookrelay.com/docs/tutorials/docs/tutorials/email/api/) — POST email to your own endpoint - [Email to a database](https://webhookrelay.com/docs/tutorials/docs/tutorials/email/database/) — insert every email into your DB ## [CI/CD](#cicd) Securely deliver webhooks to build servers running behind a firewall or without a public IP: - [Jenkins and GitHub](https://webhookrelay.com/docs/tutorials/docs/tutorials/cicd/jenkins-github/) — trigger Jenkins builds from GitHub webhooks - [Jenkins and Bitbucket](https://webhookrelay.com/docs/tutorials/docs/tutorials/cicd/jenkins-bitbucket/) — connect Bitbucket to Jenkins - [Execute scripts on webhook](https://webhookrelay.com/docs/tutorials/docs/tutorials/cicd/webhook-exec/) — run bash, Python or Ruby on incoming webhooks ## [Edge & IoT](#edge-iot) Receive webhooks directly on devices and local machines with no public IP: - [Home Assistant](https://webhookrelay.com/docs/tutorials/docs/tutorials/edge/home-assistant/) — remote access without exposing your network - [Node-RED](https://webhookrelay.com/docs/tutorials/docs/tutorials/edge/node-red/) — process webhooks inside Node-RED flows - [JavaScript app](https://webhookrelay.com/docs/tutorials/docs/tutorials/edge/javascript-app/) — stream webhooks straight into your app ## [Transform](#transform) Reshape and enrich webhook payloads in flight: - [DockerHub to Slack](https://webhookrelay.com/docs/tutorials/docs/tutorials/transform/docker-to-slack/) — convert and forward to Slack - [Enrich webhooks](https://webhookrelay.com/docs/tutorials/docs/tutorials/transform/enrich-webhooks/) — call a third-party API before forwarding ## [Data warehouse](#data-warehouse) - [GCP BigQuery](https://webhookrelay.com/docs/tutorials/docs/tutorials/warehouse/bigquery/) — insert and stream webhook data into BigQuery Did this page help you? --- --- title: Webhook Throttling | WebhookRelay meta: "og: title": "Webhook Throttling" description: Smooth out webhook spikes before they reach your servers. Throttling paces delivery to a steady rate or fixed concurrency you choose. url: https://webhookrelay.com/features/throttling.md file: /features/throttling.md --- ![Stripes](https://webhookrelay.com/features/throttling/images/stripes.svg) FEATURES # **Webhook Throttling** Smooth out webhook spikes before they reach your servers. Throttling paces delivery to a steady rate or fixed concurrency you choose. Throttled delivery · spikes in, steady out 8/s delivered · cap 50/s ▲ 8/s in0 queuedat 63s 0 175 350 400 800 0s 10s 20s 30s 40s 50s 60s throttle · 50/s InboundDelivered (capped)Queued260/s peak in · steady 50/s out · 2701 delivered, 0 dropped **Traffic doesn't arrive politely.** Webhooks come in bursts — and a burst that's harmless to Webhook Relay can flatten the server on the other end. Throttling lets you set the pace. ## [The problem: spikes don't ask permission](#the-problem-spikes-dont-ask-permission) A quiet integration can turn into a flood without warning. A provider replays a backlog, a sale goes live, a batch job fires thousands of events at once. Webhook Relay absorbs that spike without breaking a sweat — but your destination might not. The result is familiar: timeouts, `429`s, a database pegged at 100%, and a cascade of failures that takes out more than just the webhook endpoint. The usual fixes are all work: stand up a queue, add workers, write the backpressure logic, tune it, then babysit it. ## [The solution: deliver at a pace you control](#the-solution-deliver-at-a-pace-you-control) Throttling puts a governor between Webhook Relay and your destination. Instead of delivering the instant an event arrives, Webhook Relay holds each webhook safely and releases it at the speed you set — turning a jagged spike into a smooth, predictable stream your server can actually handle. ![Steady, metered throughput](https://webhookrelay.com/features/throttling/images/features/throttling/throughput.png) ### [Two ways to set the pace](#two-ways-to-set-the-pace) - **Rate** — deliver at most _N_ webhooks per second, minute or hour. Set it to whatever your endpoint (or a downstream API's rate limit) can take, and Webhook Relay never goes over. - **Concurrency** — cap how many deliveries are in flight at once. Set it to 1 for strictly one-at-a-time delivery, or to a handful to match exactly how much parallel load your service can handle. Either way, nothing is thrown away to keep the pace. Throttled webhooks wait their turn in order, oldest first, and go out the moment there's room. ### [Backpressure when you need it](#backpressure-when-you-need-it) Add an optional queue limit and Webhook Relay will push back at the source — returning a standard `429 Too Many Requests` once a destination's queue is full, so a runaway sender slows down instead of building an endless backlog. Anything still waiting past its deadline is cleaned up automatically, so queues never rot. ## [Protects every destination](#protects-every-destination) Throttling works the same for **public HTTPS endpoints** and for **internal services** reached through the [Webhook Relay agent](https://webhookrelay.com/features/throttling/features/webhook-to-internal-server/) — especially handy when the thing you're protecting is a small service on your own infrastructure. ## [Where it earns its keep](#where-it-earns-its-keep) - **Fragile or legacy endpoints.** That internal service which falls over above 20 requests a second? Cap it at 20 and stop worrying. - **Downstream rate limits.** Forwarding into an API that allows 100 calls a minute? Match it exactly and never get rate-limited again. - **Big bursts, small servers.** A flash sale or a webhook replay shouldn't decide whether your server stays up. - **Fair sharing.** Give each destination its own budget so one slow consumer can't starve the rest. ## [Better together with durable retries](#better-together-with-durable-retries) Throttling decides _how fast_; [durable retries](https://webhookrelay.com/features/throttling/features/durable-retries/) decide _how long_. Turn on both and retries to a recovering endpoint respect the same speed limit as fresh traffic — so you never accidentally finish off a server that's only just getting back on its feet. Throttling is one capability of Webhook Relay's [webhook gateway](https://webhookrelay.com/features/throttling/webhook-gateway/), alongside durable delivery, authentication, fan-out and delivery observability. Ready to take control of your throughput? [Create a free account](https://my.webhookrelay.com/register) and add throttling to your first destination. ![Stripes](https://webhookrelay.com/features/throttling/images/stripes-dark.svg) ## **Start forwarding webhooks in minutes ** Connect a source, pick a destination, and Webhook Relay handles delivery, retries and transforms. Set up your first webhook in under five minutes. [Start for free ->](https://my.webhookrelay.com/register) Free plan · No credit card required · 7-day money-back guarantee on paid plans --- --- title: Custom Domains | WebhookRelay meta: "og: title": "Custom Domains" description: Receive webhooks on your own custom domain with Webhook Relay. Brand your endpoints, keep URLs stable and route traffic through domains you control. url: https://webhookrelay.com/features/custom-domains.md file: /features/custom-domains.md --- ![Stripes](https://webhookrelay.com/features/custom-domains/images/stripes.svg) FEATURES # **Custom Domains** Receive webhooks on your own custom domain with Webhook Relay. Brand your endpoints, keep URLs stable and route traffic through domains you control. ![Custom Domains](https://webhookrelay.com/features/custom-domains/images/features/custom-domains/cover.png) Using your own domain (e.g., `webhooks.your-company.com`) for your webhook endpoints takes your integration capabilities to the next level. It offers a completely white-labeled experience, ensuring that your webhook URLs are entirely under your brand and control. Webhook Relay handles the complexities of SSL/TLS certificate management, providing automatic HTTPS for your custom domain. ### [Why Use a Custom Domain?](#why-use-a-custom-domain) While custom subdomains offer a significant step up in usability and branding, using your own custom domain provides the ultimate level of professionalism and control. Instead of a URL like `your-company.hooks.webhookrelay.com`, your webhooks can be received at `webhooks.your-company.com` or any other domain you own. This makes the webhook endpoints appear as if they are an integral part of your own server infrastructure. ### [Key Benefits:](#key-benefits) - **Complete Branding:** Present a fully branded experience to your users and webhook providers. The webhook URLs will use your domain, reinforcing brand trust and consistency. - **Appear as Your Own Server:** By using your own domain, the webhook endpoints seem to be hosted directly on your infrastructure, offering a seamless integration experience. - **Automatic HTTPS:** Webhook Relay automatically provisions and renews SSL/TLS certificates for your custom domain, ensuring your webhook data is always transmitted securely over HTTPS without any manual intervention on your part. - **Stable & Reusable URLs:** Just like custom subdomains, your custom domain URL remains constant. You can point it to different buckets within Webhook Relay as your needs evolve, without requiring changes in your webhook provider configurations. - **Enhanced Trust & Professionalism:** Using your own domain for webhook endpoints projects a higher level of professionalism and can increase trust with third-party services sending you webhooks. - **Simplified Configuration & Management:** While the initial setup involves pointing your DNS records to Webhook Relay, subsequent management is straightforward. Redirecting webhooks to new buckets or services is done within your Webhook Relay dashboard, abstracting the underlying infrastructure. By leveraging custom domains, you establish a highly professional, secure, and flexible webhook infrastructure that is fully integrated with your brand. ![Stripes](https://webhookrelay.com/features/custom-domains/images/stripes-dark.svg) ## **Start forwarding webhooks in minutes ** Connect a source, pick a destination, and Webhook Relay handles delivery, retries and transforms. Set up your first webhook in under five minutes. [Start for free ->](https://my.webhookrelay.com/register) Free plan · No credit card required · 7-day money-back guarantee on paid plans --- --- title: Serverless Webhook Transformations | WebhookRelay meta: "og: title": "Serverless Webhook Transformations" description: Transform, filter and reshape webhook payloads in flight with Webhook Relay functions before forwarding them to any destination — no extra infrastructure. url: https://webhookrelay.com/features/transform-webhooks.md file: /features/transform-webhooks.md --- ![Stripes](https://webhookrelay.com/features/transform-webhooks/images/stripes.svg) FEATURES # **Serverless Webhook Transformations** Transform, filter and reshape webhook payloads in flight with Webhook Relay functions before forwarding them to any destination — no extra infrastructure. ![transform webhooks editor](https://webhookrelay.com/features/transform-webhooks/images/features/transform-webhooks/cover.png) ## [Unlocking Infinite Integration Possibilities](#unlocking-infinite-integration-possibilities) Webhook Relay empowers you to take full control of your webhook workflows using Lua functions. Go beyond simple forwarding and tailor webhook behavior precisely to your needs. Whether you need to reshape data structures, filter specific events, or enrich payloads with external information, Lua provides the flexibility to implement virtually any logic. ![View logs](https://webhookrelay.com/features/transform-webhooks/images/features/transform-webhooks/view-logs.png) ### [Seamlessly Connect Disparate Systems](#seamlessly-connect-disparate-systems) Bridge the gap between different platforms and services effortlessly. Imagine transforming order notifications from your e-commerce platform into custom alerts for your trading platform, or automatically routing messages from your website's HTML contact form directly into a dedicated Slack channel. With Lua transformations, you can map data fields, adjust formats, and ensure information flows smoothly between systems that weren't originally designed to talk to each other. ### [Helper Functions](#helper-functions) Webhook Relay provides a set of helper functions that make it easier to transform webhooks: - [JSON encode/decode](https://webhookrelay.com/features/transform-webhooks/docs/webhooks/functions/manipulating-json/) - [Base64 encode/decode and crypto functions](https://webhookrelay.com/features/transform-webhooks/docs/webhooks/functions/crypto-functions/) - [Make HTTP requests from within a function](https://webhookrelay.com/features/transform-webhooks/docs/webhooks/functions/make-http-request/) - [Send emails](https://webhookrelay.com/features/transform-webhooks/docs/webhooks/functions/send-emails/) - [BigQuery](https://webhookrelay.com/features/transform-webhooks/docs/webhooks/functions/big-query/) - And more. ### [Serverless Simplicity, Pay-Per-Use Efficiency](#serverless-simplicity-pay-per-use-efficiency) Forget about managing infrastructure. Webhook Relay's transformation functions run entirely in our managed environment. There are no servers to deploy, scale, or maintain. Focus solely on building your integration logic. Plus, with our pay-per-use pricing, you only pay for the compute time you actually consume, making it a cost-effective solution for businesses of all sizes. ### [Key Benefits:](#key-benefits) - **Infinite Customization:** Implement any transformation logic using the powerful and lightweight Lua scripting language. - **Deep Integration:** Connect incompatible systems like e-commerce stores, trading platforms, CRMs, and communication tools (e.g., Slack). - **Serverless:** No infrastructure management required. Deploy and run transformations instantly. - **Cost-Effective:** Pay only for the resources you use. - **Versatile Use Cases:** Ideal for connecting e-commerce events, financial alerts, contact forms, IoT device data, and much more. Start transforming your webhooks today and build sophisticated, automated workflows with ease. ![Stripes](https://webhookrelay.com/features/transform-webhooks/images/stripes-dark.svg) ## **Start forwarding webhooks in minutes ** Connect a source, pick a destination, and Webhook Relay handles delivery, retries and transforms. Set up your first webhook in under five minutes. [Start for free ->](https://my.webhookrelay.com/register) Free plan · No credit card required · 7-day money-back guarantee on paid plans --- --- title: Single Sign-On (SSO) | WebhookRelay meta: "og: title": "Single Sign-On (SSO)" description: Enable SAML-based Single Sign-On (SSO) for your Webhook Relay account with Okta, Azure AD and more — centralize authentication and simplify user management. url: https://webhookrelay.com/features/sso.md file: /features/sso.md --- ![Stripes](https://webhookrelay.com/features/sso/images/stripes.svg) **Plans: Enterprise** FEATURES # **Single Sign-On (SSO)** Enable SAML-based Single Sign-On (SSO) for your Webhook Relay account with Okta, Azure AD and more — centralize authentication and simplify user management. ![SSO](https://webhookrelay.com/features/sso/images/features/sso/cover.png) Webhook Relay supports SAML based SSO with Microsoft Active Directory, Okta and similar providers. ## [Benefits of SSO for Enterprises](#benefits-of-sso-for-enterprises) Integrating Webhook Relay with your existing Single Sign-On (SSO) solution via SAML offers significant advantages for enterprise security and user management: - **Enhanced Security:** SSO centralizes user authentication, reducing the attack surface. By integrating with established Identity Providers (IdPs) like Microsoft Azure AD or Okta, you leverage their robust security measures, including multi-factor authentication (MFA) and conditional access policies. This minimizes password fatigue and the risk associated with weak or reused passwords. - **Improved User Experience:** Users log in once using their familiar corporate credentials and gain access to Webhook Relay without needing separate usernames and passwords. This streamlines workflows and reduces friction, improving productivity. - **Simplified User Management:** IT administrators can manage user access centrally through their existing IdP. Provisioning and de-provisioning users for Webhook Relay becomes part of the standard identity management process, reducing administrative overhead and ensuring timely access revocation when employees leave. - **Centralized Access Control:** Define granular access policies within your IdP and enforce them across all connected applications, including Webhook Relay. This ensures consistent security posture and simplifies compliance audits. - **Compliance and Auditing:** Centralized authentication logs provide a clear audit trail for user access, helping organizations meet compliance requirements like SOC 2 or GDPR. ![Stripes](https://webhookrelay.com/features/sso/images/stripes-dark.svg) ## **Start forwarding webhooks in minutes ** Connect a source, pick a destination, and Webhook Relay handles delivery, retries and transforms. Set up your first webhook in under five minutes. [Start for free ->](https://my.webhookrelay.com/register) Free plan · No credit card required · 7-day money-back guarantee on paid plans --- --- title: Transform Webhooks with AI | WebhookRelay meta: "og: title": "Transform Webhooks with AI" description: Automatically transform webhook payloads using AI with Webhook Relay — map fields, reformat data and integrate incompatible systems without writing parsers. url: https://webhookrelay.com/features/transform-webhooks-with-ai.md file: /features/transform-webhooks-with-ai.md --- ![Stripes](https://webhookrelay.com/features/transform-webhooks-with-ai/images/stripes.svg) FEATURES # **Transform Webhooks with AI** Automatically transform webhook payloads using AI with Webhook Relay — map fields, reformat data and integrate incompatible systems without writing parsers. ![Transform webhooks with AI](https://webhookrelay.com/features/transform-webhooks-with-ai/images/features/transform-with-ai/cover.png) ## [The Challenge: Incompatible Webhook Formats](#the-challenge-incompatible-webhook-formats) Integrating different SaaS applications and services often involves dealing with webhooks. However, each service typically has its own unique webhook payload structure and format. Manually writing code to translate these formats (e.g., converting a Stripe webhook into a Slack notification format) is time-consuming, error-prone, and requires coding expertise, creating a significant barrier to seamless automation. Keywords: webhook transformation, data mapping, API integration, incompatible formats, integration challenges, manual coding. ## [The Solution: Webhook Relay's AI-Powered Transformation](#the-solution-webhook-relays-ai-powered-transformation) Webhook Relay introduces **Transform with AI**, a revolutionary feature that automates the creation of webhook transformation logic. Instead of writing Lua code yourself, you simply describe the transformation you need – what the incoming webhook looks like and what the desired output format is. Our AI engine then automatically generates the necessary Lua function code to perform this conversion. This allows you to bridge the gap between disparate systems effortlessly, enabling complex integrations without writing a single line of code. ### [How it Works:](#how-it-works) 1. **Provide examples:** Provide an example webhook of what you are receiving and the desired output format. 2. **Generate:** Webhook Relay's AI analyzes your request and generates the corresponding transformation function. 3. **Apply:** The generated code is added to your webhook function, instantly enabling the transformation for all incoming webhooks. ### [Benefits:](#benefits) - **Save Time:** Eliminate hours spent deciphering API docs and writing custom transformation code. Get integrations up and running in minutes. - **Automate Integration:** Seamlessly connect services with incompatible webhook formats, automating workflows between your favorite tools. - **No Coding Required:** The simplest way to integrate 3rd party SaaS apps. Let AI handle the code generation, making powerful integrations accessible to everyone. - **Effortless Integration:** Focus on your workflow logic, not the intricacies of data formats. AI ensures payloads are correctly translated between services. - **Reliability:** Leverage AI to create accurate and efficient transformation logic based on best practices. With Webhook Relay's "Transform with AI," integrating diverse services becomes incredibly simple and fast. Unlock the full potential of your automated workflows without the traditional complexities of webhook transformation. ![Stripes](https://webhookrelay.com/features/transform-webhooks-with-ai/images/stripes-dark.svg) ## **Start forwarding webhooks in minutes ** Connect a source, pick a destination, and Webhook Relay handles delivery, retries and transforms. Set up your first webhook in under five minutes. [Start for free ->](https://my.webhookrelay.com/register) Free plan · No credit card required · 7-day money-back guarantee on paid plans --- --- title: Teams | WebhookRelay meta: "og: title": Teams description: Create teams and invite colleagues to your Webhook Relay account so you can manage buckets, inputs, outputs and forwarding rules together, securely. url: https://webhookrelay.com/features/teams.md file: /features/teams.md --- ![Stripes](https://webhookrelay.com/features/teams/images/stripes.svg) **Plans: Business, Pro and Enterprise** FEATURES # **Teams** Create teams and invite colleagues to your Webhook Relay account so you can manage buckets, inputs, outputs and forwarding rules together, securely. ![Teams](https://webhookrelay.com/features/teams/images/features/teams/cover.png) Webhook Relay supports teams so you can invite team members to your account. ## [Streamline Collaboration with Teams](#streamline-collaboration-with-teams) The Teams feature allows you to create sub-accounts for your colleagues, granting them access to manage and monitor your Webhook Relay resources collaboratively. This fosters better teamwork and faster issue resolution. ### [Key Capabilities for Team Members:](#key-capabilities-for-team-members) - **Secure Login:** Invited members can log in securely using their own credentials, accessing the shared account resources. - **Troubleshooting Access:** Team members gain visibility into request logs, enabling them to effectively diagnose and troubleshoot integration issues. - **Configuration Management:** Empower your team to update and fix configuration problems directly, reducing dependencies and speeding up maintenance. - **[Custom roles](https://webhookrelay.com/features/teams/features/custom-roles/):** Assign Admin, Billing, Member or Viewer so each person only gets the access they need. By enabling team collaboration, you distribute workload, enhance monitoring capabilities, and ensure smoother operation of your webhook integrations. ![Stripes](https://webhookrelay.com/features/teams/images/stripes-dark.svg) ## **Start forwarding webhooks in minutes ** Connect a source, pick a destination, and Webhook Relay handles delivery, retries and transforms. Set up your first webhook in under five minutes. [Start for free ->](https://my.webhookrelay.com/register) Free plan · No credit card required · 7-day money-back guarantee on paid plans --- --- title: Free CloudEvents Validator — Validate JSON | WebhookRelay meta: "og: title": "Free CloudEvents Validator — Validate JSON" description: Free online CloudEvents validator. Paste a CloudEvent JSON and check it against the CloudEvents 1.0 spec — attributes, types, RFC 3339 time. No signup. url: https://webhookrelay.com/cloudevents-validator.md file: /cloudevents-validator.md --- # **CloudEvents Validator** Free online **CloudEvents validator**. Paste a CloudEvent in JSON and instantly check it against the **CloudEvents 1.0** specification — required attributes (`id`, `source`, `specversion`, `type`), attribute types, RFC 3339 timestamps, URI references and extension naming rules. Single events and batches are both supported. Everything runs in your browser — nothing is uploaded, and no signup is required. **Paste your CloudEvent (JSON)** Load example: **✓** **Valid CloudEvent** Checked against CloudEvents 1.0 (JSON format). **✓**`specversion` **✓**`id` **✓**`source` **✓**`type` No issues found. Building an event-driven service? Learn how to [transform incoming webhooks into CloudEvents](https://webhookrelay.com/cloudevents-validator/features/transform-webhooks) with Webhook Relay, or [capture and inspect events online](https://webhookrelay.com/cloudevents-validator/webhook-bin/) with our free Webhook Bin. ## **What are CloudEvents?** **CloudEvents** is an open specification for describing event data in a common, vendor-neutral way. It is a [Cloud Native Computing Foundation](https://www.cncf.io/) (CNCF) project, and its goal is simple: when one system emits an event — an order was placed, a file was uploaded, a pull request was opened — every other system should be able to read the same envelope without custom glue code. Before CloudEvents, every provider invented its own event shape, so consumers had to learn a new format for AWS, Azure, GitHub, Stripe and everything else. CloudEvents standardises the _metadata around the event_ (who sent it, what kind of event it is, when it happened, a unique id) while leaving the actual payload (`data`) entirely up to you. A CloudEvent has four **required attributes** — `specversion`, `id`, `source` and `type` — plus a handful of optional ones such as `subject`, `time`, `datacontenttype` and `dataschema`. The pair `source` + `id` must be unique for every distinct event, which is what makes reliable de-duplication and replay possible. The spec is widely supported: Azure Event Grid, Knative Eventing, KEDA, Argo Events, Alibaba Cloud and many event meshes and message brokers can emit or consume CloudEvents natively, and there are official SDKs for Go, JavaScript, Python, Java, C#, Rust, PHP and Ruby. ## **How to work with CloudEvents** CloudEvents can travel over almost any transport — HTTP, Kafka, AMQP, MQTT, NATS, WebSockets — using one of two content modes. In **structured mode** the entire event (attributes and data) is encoded together, usually as a single JSON document with `Content-Type: application/cloudevents+json`; this is what you paste into the validator above. In **binary mode** the event `data` becomes the raw HTTP body and the metadata is carried in `ce-*` headers (for example `ce-id`, `ce-source`, `ce-type`), which keeps the payload untouched for systems that already know how to read it. Binary payloads that aren't text are carried in JSON as a Base64 string under `data_base64` instead of `data`. In practice you rarely hand-build the JSON: you use an SDK to construct, serialise and parse events, and your broker or gateway handles the transport binding. Validation still matters, though — a missing `type`, a non-RFC 3339 `time`, an uppercase attribute name or a relative `source` are common mistakes that break downstream consumers and routing rules. Paste your event above to catch them before they ship. If you receive webhooks from third parties and want to normalise them into CloudEvents, you can do it server-side with a [Webhook Relay transformation function](https://webhookrelay.com/cloudevents-validator/features/transform-webhooks) — reshape the body, set the `ce-*` attributes and forward the event on to any destination. ## **CloudEvents JSON example** A minimal but complete CloudEvent in the JSON format looks like this: ``` { "specversion": "1.0", "id": "A234-1234-1234", "source": "https://github.com/cloudevents/spec/pull/123", "type": "com.github.pull_request.opened", "subject": "123", "time": "2025-06-21T17:31:00Z", "datacontenttype": "application/json", "data": { "pullRequestId": 123, "title": "Add CloudEvents validator" } } ``` ## **CloudEvents attribute reference (v1.0)** | **Attribute** | **Required** | **Type** | **Notes** | | --- | --- | --- | --- | | `specversion` | Yes | String | The CloudEvents version. Currently "1.0". | | `id` | Yes | String | Unique per source. source + id identifies an event. | | `source` | Yes | URI-reference | Where the event came from. Absolute URI recommended. | | `type` | Yes | String | The kind of event, e.g. com.example.order.created. | | `subject` | No | String | Subject of the event within the source, for filtering. | | `time` | No | Timestamp | When it happened, in RFC 3339 format. | | `datacontenttype` | No | String | Media type of data (RFC 2046), e.g. application/json. | | `dataschema` | No | URI | Schema the data adheres to. | | `data` | No | Any | The event payload. Mutually exclusive with data_base64. | | `data_base64` | No | String | Base64-encoded payload for binary data. | ## **Frequently asked questions**
**What is CloudEvents?** CloudEvents is a [CNCF](https://www.cncf.io/) specification for describing event data in a common, vendor-neutral format. It standardises the metadata around an event — id, source, type, time, specversion — while leaving the payload up to you, so different systems can exchange events without writing a custom adapter for each provider.
**What makes a CloudEvent valid?** A valid CloudEvent includes the four required attributes — `specversion`, `id`, `source` and `type` — each a non-empty string, with `source` being a URI-reference. Optional attributes must match their declared types when present, attribute names must be lowercase letters and digits only, `time` must be an RFC 3339 timestamp, and an event must not contain both `data` and `data_base64`.
**Which CloudEvents version does this validate against?** CloudEvents 1.0 (the current major version, covering 1.0.x) in the JSON event format. If your event declares a different `specversion`, the validator flags it.
**Does it support structured and binary mode?** This tool validates the structured JSON representation (one JSON document containing both attributes and data). Binary mode carries attributes in `ce-*` HTTP headers and the payload in the body — to validate binary mode, paste the equivalent structured JSON.
**Is my data sent anywhere?** No. Validation runs entirely in your browser with JavaScript. Your event JSON never leaves your machine, and no signup is required.
**How do I turn webhooks into CloudEvents?** Use a [Webhook Relay transformation function](https://webhookrelay.com/cloudevents-validator/features/transform-webhooks) to reshape an incoming webhook body, set the CloudEvents attributes and forward it to any destination — no code to host yourself.
## **Related free tools** [Webhook Tester (Bin)](https://webhookrelay.com/cloudevents-validator/webhook-bin/) [HMAC Signature Verifier](https://webhookrelay.com/cloudevents-validator/hmac-verification/) [Verify Webhook Signatures](https://webhookrelay.com/cloudevents-validator/verify-webhook-signature/) [Slack/Discord Formatter](https://webhookrelay.com/cloudevents-validator/webhook-message-formatter/) [Transform Webhooks](https://webhookrelay.com/cloudevents-validator/features/transform-webhooks/) --- --- title: Schedule recurring webhooks | WebhookRelay meta: "og: title": "Schedule recurring webhooks" description: Webhook Relay allows you to schedule recurring webhooks to your own endpoints. Set it to run every few seconds, minutes, hours or days. url: https://webhookrelay.com/cron.md file: /cron.md --- # **Recurring Webhooks** Automate your workflows with our powerful cron webhook feature. Set up recurring tasks, send notifications, and execute serverless functions on a schedule. [Configure Cron ->](https://my.webhookrelay.com/cron) [Learn More](https://webhookrelay.com/cron/docs/webhooks/cron/using-cron-webhooks) ![Cron](https://webhookrelay.com/cron/images/landing/cron/cron.png) ## **Track webhook status** View status, requests and responses for your cron jobs. Once requests fail, view the error details and retry. ![Cron](https://webhookrelay.com/cron/images/landing/cron/cron_sent.png) ### Schedule specific time Send webhooks at a specific date and time and timezone. Whether it's a daily report or a monthly update, you can set it up in seconds. ### Multiple URLs Send webhooks to multiple URLs with a single cron job. Same cron schedule can send multiple webhooks to different URLs. ### Execute serverless functions Cron webhooks can trigger Functions to customize your workflows. Whether it's accessing 3rd party APIs or inserting into data warehouse, choice is yours. ![Stripes](https://webhookrelay.com/cron/images/stripes-dark.svg) ## **Start forwarding webhooks in minutes ** Connect a source, pick a destination, and Webhook Relay handles delivery, retries and transforms. Set up your first webhook in under five minutes. [Start for free ->](https://my.webhookrelay.com/register) Free plan · No credit card required · 7-day money-back guarantee on paid plans --- --- title: Webhook Relay Kubernetes Integration | WebhookRelay meta: "og: title": "Webhook Relay Kubernetes Integration" description: Seamlessly connect your Kubernetes services to external webhooks without exposing them directly to the internet using the Webhook Relay Operator. url: https://webhookrelay.com/features/webhook-kubernetes-integration.md file: /features/webhook-kubernetes-integration.md --- ![Stripes](https://webhookrelay.com/features/webhook-kubernetes-integration/images/stripes.svg) FEATURES # **Webhook Relay Kubernetes Integration** Seamlessly connect your Kubernetes services to external webhooks without exposing them directly to the internet using the Webhook Relay Operator. ![Webhook Relay Kubernetes Operator](https://webhookrelay.com/features/webhook-kubernetes-integration/images/features/kubernetes/cover.png) ## [The Challenge: Exposing Kubernetes Services Securely](#the-challenge-exposing-kubernetes-services-securely) Connecting internal Kubernetes services to external webhook providers often requires complex network configurations. You might need to set up Load Balancers, configure Ingress controllers, manage TLS certificates, and potentially open firewall ports, exposing your cluster to the public internet. This increases the attack surface and operational overhead, especially for on-premise clusters or edge deployments lacking stable public IPs. ## [The Solution: Webhook Relay Operator](#the-solution-webhook-relay-operator) The Webhook Relay Operator simplifies and secures webhook integration for Kubernetes. It runs inside your cluster and establishes a secure, persistent tunnel to the Webhook Relay service. Incoming webhooks are received by Webhook Relay and securely forwarded through this tunnel directly to your target service within the cluster, without requiring any inbound ports to be opened on your firewall. ### [How it Works:](#how-it-works) 1. [Install the Webhook Relay Operator](https://webhookrelay.com/features/webhook-kubernetes-integration/docs/installation/kubernetes/) in your Kubernetes cluster. 2. Create the CRD to enable forwarding: ``` apiVersion: forward.webhookrelay.com/v1 kind: WebhookRelayForward metadata: name: example-forward spec: buckets: - name: k8s-operator inputs: - name: public-endpoint description: "Public endpoint, supply this to the webhook producer" responseBody: "OK" responseStatusCode: 200 outputs: - name: webhook-receiver destination: http://my-service:5050/webhooks ``` 1. Operator creates the desired configuration and starts forwarding the webhooks to your target service ### [Benefits:](#benefits) - **Eliminate Complex Setup:** No need to create or manage Load Balancers or Ingress controllers just for receiving webhooks. The Operator handles the connection securely. - **Enhanced Security:** Keep your Kubernetes services private. No need to expose services directly to the internet, significantly reducing the attack surface. This makes it the **most secure way** to integrate services like Jenkins CI running inside Kubernetes with external webhook sources (e.g., GitHub, GitLab). - **On-Premise & Private Clusters:** Perfect for **on-premise Kubernetes clusters** that reside behind corporate firewalls or lack direct public internet access. - **Edge Kubernetes Ready:** Ideal for **edge Kubernetes distributions** like k3s or MicroK8s, which often run on devices without stable public IP addresses. - **Simplified Management:** Define your webhook forwarding rules declaratively using Kubernetes custom resources. By using the Webhook Relay Kubernetes Operator, you gain a secure, simple, and reliable way to integrate your internal Kubernetes applications with the outside world via webhooks, regardless of your network topology. ![Stripes](https://webhookrelay.com/features/webhook-kubernetes-integration/images/stripes-dark.svg) ## **Start forwarding webhooks in minutes ** Connect a source, pick a destination, and Webhook Relay handles delivery, retries and transforms. Set up your first webhook in under five minutes. [Start for free ->](https://my.webhookrelay.com/register) Free plan · No credit card required · 7-day money-back guarantee on paid plans --- --- title: Rewriting Host Header | WebhookRelay meta: "og: title": "Rewriting Host Header" description: Rewrite the Host header on forwarded webhooks and tunnels with Webhook Relay so you can expose local servers and virtual hosts to the internet correctly. url: https://webhookrelay.com/features/rewrite-host-header.md file: /features/rewrite-host-header.md --- ![Stripes](https://webhookrelay.com/features/rewrite-host-header/images/stripes.svg) FEATURES # **Rewriting Host Header** Rewrite the Host header on forwarded webhooks and tunnels with Webhook Relay so you can expose local servers and virtual hosts to the internet correctly. Host header verification is a common feature on web servers. For example in PHP or Node.js you will encounter errors such as: ``` Blocked request. This host ("xyz.webrelay.io") is not allowed. To allow this host, add "xyz.webrelay.io" to \`server.allowedHosts\` in vite.config.js. ``` ## [Host Header Mismatch](#host-header-mismatch) When tunneling to `localhost`, requests arrive with the public tunnel address (e.g., `xyz.webrelay.io`) as the `Host` header. Many servers and frameworks (like Vite, Nuxt.js, Next.js, Apache, Nginx) perform **host header validation**. They block requests if the `Host` header doesn't match the expected server name (like `localhost`), preventing access via the public tunnel URL. Keywords: host header validation, blocked request, localhost, tunnel, expose local server, Vite allowedHosts, CORS, reverse proxy. ## [The Solution: Rewriting the Host Header](#the-solution-rewriting-the-host-header) Webhook Relay tunnels provide a simple solution with the `--rewrite-host-header` flag for the `relay connect` command. When enabled, the tunnel automatically changes the `Host` header of incoming requests to match your local destination server's hostname (e.g., `localhost`) before forwarding the request. This satisfies your local server's host header validation, allowing requests through the tunnel. To enable this feature, add the flag to your command: ``` relay connect -n blog --rewrite-host-header localhost http://localhost:3000 ``` ### [Benefits:](#benefits) - **No configuration changes:** Avoids modifying your local web server or framework settings. - **Works out of the box:** Simple flag activation is all that's needed. - **Instant sharing:** Easily share your local development environment with colleagues or integrate with external webhooks. With this command, requests to your public tunnel URL will have their `Host` header rewritten to `localhost` before reaching your application on `http://localhost:3000`, resolving validation errors seamlessly. ![Stripes](https://webhookrelay.com/features/rewrite-host-header/images/stripes-dark.svg) ## **Start forwarding webhooks in minutes ** Connect a source, pick a destination, and Webhook Relay handles delivery, retries and transforms. Set up your first webhook in under five minutes. [Start for free ->](https://my.webhookrelay.com/register) Free plan · No credit card required · 7-day money-back guarantee on paid plans --- --- title: Audit Logs | WebhookRelay meta: "og: title": "Audit Logs" description: See every action in your Webhook Relay account — who created, updated or deleted resources, when, and from which IP. Full activity tracking for organizations and teams. url: https://webhookrelay.com/features/audit-logs.md file: /features/audit-logs.md --- ![Stripes](https://webhookrelay.com/features/audit-logs/images/stripes.svg) **Plans: Business, Pro and Enterprise** FEATURES # **Audit Logs** See every action in your Webhook Relay account — who created, updated or deleted resources, when, and from which IP. Full activity tracking for organizations and teams. ![Webhook Relay Activity page showing audit log entries with time, user, action, resource, name and IP for each change](https://webhookrelay.com/features/audit-logs/images/features/audit-logs/activity.png) Organizations can view **all actions** across their Webhook Relay account — creates, updates, deletes and more — from a single Activity feed. Filter by user, action or resource and open any row to see exactly what changed. ## [Track everything that happens](#track-everything-that-happens) Every important change is attributed to a person (or system), with a timestamp and source IP: - **Who did what** — owner, organization members and sub-accounts - **What changed** — buckets, inputs, outputs, alert policies, incidents, tokens and more - **When and where** — precise time plus the IP the action came from ## [Understand usage and stay in control](#understand-usage-and-stay-in-control) Audit logs make day-to-day operations and security reviews straightforward: - **Usage visibility** — see how the account is being used and which resources change most - **Debugging** — when something breaks, the last config change is often the answer - **Accountability** — pair with [teams](https://webhookrelay.com/features/audit-logs/features/teams/) and [member roles](https://webhookrelay.com/features/audit-logs/features/team-member-roles/) so every action is attributable - **Compliance** — a clear trail for SOC 2, ISO 27001 and internal security reviews For webhook _delivery_ history (payloads, status, retries), use [webhook logs & monitoring](https://webhookrelay.com/features/audit-logs/features/webhook-logs/). Audit logs cover _account_ activity — configuration and membership changes. Ready for full account visibility? [Create an account](https://my.webhookrelay.com/register) or [talk to sales](https://webhookrelay.com/features/audit-logs/contact-sales/) about Business and Enterprise plans. ![Stripes](https://webhookrelay.com/features/audit-logs/images/stripes-dark.svg) ## **Start forwarding webhooks in minutes ** Connect a source, pick a destination, and Webhook Relay handles delivery, retries and transforms. Set up your first webhook in under five minutes. [Start for free ->](https://my.webhookrelay.com/register) Free plan · No credit card required · 7-day money-back guarantee on paid plans --- --- title: Custom Roles | WebhookRelay meta: "og: title": "Custom Roles" description: Convert your Webhook Relay account into an organization, invite other accounts as members, and assign Admin, Billing, Member or Viewer roles to control access and billing. url: https://webhookrelay.com/features/custom-roles.md file: /features/custom-roles.md --- ![Stripes](https://webhookrelay.com/features/custom-roles/images/stripes.svg) **Plans: Business, Pro and Enterprise** FEATURES # **Custom Roles** Convert your Webhook Relay account into an organization, invite other accounts as members, and assign Admin, Billing, Member or Viewer roles to control access and billing. ![Invite a member dialog with role options: Admin, Billing, Member and Viewer](https://webhookrelay.com/features/custom-roles/images/features/custom-roles/invite-member.png) Convert your account into an **organization**, then invite other people into it with their own Webhook Relay login. Assign a role so each person only gets the access they need — resources, billing, membership, or read-only visibility. Unlike [sub-accounts](https://webhookrelay.com/features/custom-roles/features/teams/) (credentials you create and manage), invited members accept from their own existing account and share your plan’s member quota. ## [Roles you can assign](#roles-you-can-assign) | Role | Access | | --- | --- | | **Admin** | Full access: members, billing, resources | | **Billing** | Resources + billing management | | **Member** | Resources only — no billing or member management | | **Viewer** | Read-only — sees everything, changes nothing, no billing | ## [Why it matters](#why-it-matters) - **Control billing** — keep plan and payment changes limited to Admin or Billing roles - **Least privilege** — engineers manage webhooks without touching membership or invoices - **Safe collaboration** — viewers can inspect configuration and traffic without risking changes - **Clear ownership** — every invite is tied to a real account; pair with [audit logs](https://webhookrelay.com/features/custom-roles/features/audit-logs/) to see who did what Works alongside [SSO](https://webhookrelay.com/features/custom-roles/features/sso/), [teams](https://webhookrelay.com/features/custom-roles/features/teams/) and [audit logs](https://webhookrelay.com/features/custom-roles/features/audit-logs/) for organization-ready access control. Ready to invite your team? [Create an account](https://my.webhookrelay.com/register) or [talk to sales](https://webhookrelay.com/features/custom-roles/contact-sales/) about Business and Enterprise plans. ![Stripes](https://webhookrelay.com/features/custom-roles/images/stripes-dark.svg) ## **Start forwarding webhooks in minutes ** Connect a source, pick a destination, and Webhook Relay handles delivery, retries and transforms. Set up your first webhook in under five minutes. [Start for free ->](https://my.webhookrelay.com/register) Free plan · No credit card required · 7-day money-back guarantee on paid plans --- --- title: Free Webhook Signature Verifier Online | WebhookRelay meta: "og: title": "Free Webhook Signature Verifier Online" description: Free webhook signature verifier for Stripe, GitHub, Shopify, Slack, Twilio and more. Check any HMAC signature in your browser — no signup. url: https://webhookrelay.com/verify-webhook-signature.md file: /verify-webhook-signature.md --- # **Webhook Signature Verifier** Free online tools to **verify webhook signatures**. Pick a provider below to check a signature against your secret with the exact scheme that provider uses — or use the generic [HMAC generator](https://webhookrelay.com/verify-webhook-signature/hmac-verification/). Everything runs in your browser: the payload and secret never leave the page, and there’s no signup. [![Stripe logo](https://webhookrelay.com/verify-webhook-signature/images/landing/logos/stripe.svg)

**Verify Stripe signatures**

→ Validate the Stripe-Signature header against your endpoint signing secret.](https://webhookrelay.com/verify-webhook-signature/verify-stripe-webhook-signature/) [![GitHub logo](https://webhookrelay.com/verify-webhook-signature/images/landing/logos/github.svg)

**Verify GitHub signatures**

→ Validate the X-Hub-Signature-256 header against your webhook secret.](https://webhookrelay.com/verify-webhook-signature/verify-github-webhook-signature/) [![Shopify logo](https://webhookrelay.com/verify-webhook-signature/images/landing/logos/shopify.svg)

**Verify Shopify signatures**

→ Validate the X-Shopify-Hmac-Sha256 header against your app's API secret.](https://webhookrelay.com/verify-webhook-signature/verify-shopify-webhook-signature/) [![Slack logo](https://webhookrelay.com/verify-webhook-signature/images/landing/logos/slack.svg)

**Verify Slack signatures**

→ Validate the X-Slack-Signature header against your app's signing secret.](https://webhookrelay.com/verify-webhook-signature/verify-slack-webhook-signature/) [![Twilio logo](https://webhookrelay.com/verify-webhook-signature/images/landing/logos/twilio.svg)

**Verify Twilio signatures**

→ Validate the X-Twilio-Signature header for form-encoded webhook callbacks.](https://webhookrelay.com/verify-webhook-signature/verify-twilio-webhook-signature/) [**S**

**Verify Standard Webhooks signatures**

→ Validate the webhook-signature header used by the Standard Webhooks spec.](https://webhookrelay.com/verify-webhook-signature/verify-standard-webhooks-signature/) [![Square logo](https://webhookrelay.com/verify-webhook-signature/images/landing/logos/square.svg)

**Verify Square signatures**

→ Validate the x-square-hmacsha256-signature header against your subscription signature key.](https://webhookrelay.com/verify-webhook-signature/verify-square-webhook-signature/) [**L**

**Verify LINE signatures**

→ Validate the x-line-signature header against your channel secret.](https://webhookrelay.com/verify-webhook-signature/verify-line-webhook-signature/) [**A**

**Verify Airwallex signatures**

→ Validate the x-signature header against your webhook secret and x-timestamp.](https://webhookrelay.com/verify-webhook-signature/verify-airwallex-webhook-signature/) [**M**

**Verify MyFatoorah signatures**

→ Validate the MyFatoorah-Signature header against the canonical Key=Value string.](https://webhookrelay.com/verify-webhook-signature/verify-myfatoorah-webhook-signature/) [![Zendesk logo](https://webhookrelay.com/verify-webhook-signature/images/landing/logos/zendesk.svg)

**Verify Zendesk signatures**

→ Validate the X-Zendesk-Webhook-Signature header against your webhook signing secret.](https://webhookrelay.com/verify-webhook-signature/verify-zendesk-webhook-signature/) [![DocuSign logo](https://webhookrelay.com/verify-webhook-signature/images/landing/logos/docusign.svg)

**Verify DocuSign signatures**

→ Validate the X-DocuSign-Signature-1 header against your Connect HMAC key.](https://webhookrelay.com/verify-webhook-signature/verify-docusign-webhook-signature/) [**K**

**Verify Klarna signatures**

→ Validate the Klarna-Signature header against your notification signing key.](https://webhookrelay.com/verify-webhook-signature/verify-klarna-webhook-signature/) [**A**

**Verify Attio signatures**

→ Validate the Attio-Signature header against your webhook secret.](https://webhookrelay.com/verify-webhook-signature/verify-attio-webhook-signature/) Don’t see your provider? Most webhooks use plain HMAC over the request body — use the [HMAC generator & signature verifier](https://webhookrelay.com/verify-webhook-signature/hmac-verification/) (SHA256, SHA512, SHA1, MD5) for any of them. ## **What is a webhook signature?** A **webhook signature** is a cryptographic fingerprint a provider attaches to each webhook so you can prove the request really came from them and wasn’t altered in transit. The provider and you share a secret; the provider computes a hash — almost always **HMAC‑SHA256** — over the request body (sometimes prefixed with a timestamp and id) and sends the result in a header such as `Stripe-Signature`, `X-Hub-Signature-256` or `webhook-signature`. You recompute the same hash with your copy of the secret and compare. If they match, the webhook is authentic; if not, you reject it. Without this check, anyone who learns your endpoint URL could POST fake events to it. Schemes differ in the details — which header carries the signature, whether the output is hex or base64, whether a timestamp is included to stop replay attacks, and how the secret is encoded — which is why a Stripe verifier and a Shopify verifier aren’t interchangeable. The tools above bake in each provider’s exact rules so you don’t have to. ## **How to verify a webhook signature** 1. Capture the **raw** request body before any JSON parsing — re-serializing changes the bytes and breaks the hash. 2. Read the signature (and timestamp/id, if used) from the provider’s headers. 3. Recompute the HMAC with your shared secret using the provider’s exact construction. 4. Compare with a **constant-time** comparison (e.g. `crypto.timingSafeEqual`), and reject stale timestamps to block replays. ## **Frequently asked questions**
**How do I verify a webhook signature?** Recompute the provider's HMAC over the raw request body (plus any timestamp/id it signs) using your shared secret, then constant-time compare it to the signature header. Pick your provider above to do it instantly, or read the step-by-step above.
**Why must I use the raw request body?** Providers sign the exact bytes they send. Parsing JSON and re-serializing it changes whitespace and key order, so the recomputed hash won't match. Always verify against the unparsed body.
**What is a replay attack and how do timestamps help?** A replay attack resends a previously valid, correctly-signed webhook. Providers that sign a timestamp (Stripe, Slack, Standard Webhooks) let you reject requests older than a few minutes, which defeats replays.
**Is this safe to paste secrets into?** Yes — every verifier here runs entirely in your browser with the Web Crypto API. Your payload and secret are never sent to a server, and no signup is required. Still, prefer test secrets where you can.
## **Related free tools** [HMAC Generator](https://webhookrelay.com/verify-webhook-signature/hmac-verification/) [CloudEvents Validator](https://webhookrelay.com/verify-webhook-signature/cloudevents-validator/) [Webhook Tester (Bin)](https://webhookrelay.com/verify-webhook-signature/webhook-bin/) [Slack/Discord Formatter](https://webhookrelay.com/verify-webhook-signature/webhook-message-formatter/) --- --- title: Receive emails as webhooks | WebhookRelay meta: "og: title": "Receive emails as webhooks" description: Turn inbound email into webhooks. Create an email input on a bucket, get a unique address, and every email is parsed to JSON and sent to your endpoint. url: https://webhookrelay.com/docs/email.md file: /docs/email.md --- ![Stripes](https://webhookrelay.com/docs/email/images/stripes.svg) Documentation **Fundamentals** # **Receive emails as webhooks** Turn inbound email into webhooks. Create an email input on a bucket, get a unique address, and every email is parsed to JSON and sent to your endpoint. Webhook Relay can **receive inbound email and deliver it as a webhook**. You add an _email input_ to a bucket, Webhook Relay gives you a unique address like `5519fb0d-997a-46cd-b102-ac990b4c38fa@in.webhookrelay-mail.com`, and every message sent to it is parsed and forwarded to your endpoint as a JSON `POST` — the same pipeline as any other Webhook Relay input, so you also get transforms, fan-out, retries and an audit log. ``` Sender ──▶ {uuid}@in.webhookrelay-mail.com ──▶ Webhook Relay ──▶ JSON webhook ──▶ your endpoint (any email) (your email input) (parse + policy) (POST application/json) (or transform first) ``` Use it to drive automation from email: parse order confirmations, support requests, alerts, reports or any "email-only" system into your API, a database, Slack/Discord, or a spreadsheet — without running an SMTP server. ## [Create an email input](#create-an-email-input) 1. Open the [Webhook Relay dashboard](https://my.webhookrelay.com) and create (or pick) a **bucket** — a bucket groups inputs and outputs. 2. Add an **Email** input to the bucket. Webhook Relay generates a unique inbound address for it. ![Creating an email input on a bucket in the Webhook Relay dashboard](https://webhookrelay.com/docs/email/images/docs/email/email_input.png) 1. Copy the generated address — it looks like `@in.webhookrelay-mail.com`, where the `` is unique to that input. 2. Send mail to it (or hand it to whatever system needs to email you). Every message is parsed and delivered to the bucket's outputs. > Each email input has its own address. Disable the input to stop accepting mail without deleting it, and delete it to free the address. Prefer the terminal? The `relay` CLI creates an address in a single command and can poll the bucket for incoming mail — see [Create & poll email addresses from the CLI](https://webhookrelay.com/docs/email/docs/email/cli/). ## [What your endpoint receives](#what-your-endpoint-receives) The parsed message is delivered as a JSON body with `Content-Type: application/json` and an `X-Webhookrelay-Source: email` header. Here is a real message: ``` { "from": "karolis.rusenas@gmail.com", "from_name": "K", "recipient": "5519fb0d-997a-46cd-b102-ac990b4c38fa@in.webhookrelay-mail.com", "to": ["5519fb0d-997a-46cd-b102-ac990b4c38fa@in.webhookrelay-mail.com"], "subject": "prod test", "date": "Fri, 26 Jun 2026 11:27:41 +0400", "message_id": "C70BE3E7-4B20-4240-A1B8-2D19E0032FFD@gmail.com", "text": "helloooo\r\n", "headers": { "From": "K ", "To": "5519fb0d-997a-46cd-b102-ac990b4c38fa@in.webhookrelay-mail.com", "Subject": "prod test", "Content-Type": "text/plain; charset=us-ascii", "Return-Path": "" }, "spf": "none", "dkim": "pass", "dmarc": "pass" } ``` HTML emails also include an `html` field, and attachments arrive in an `attachments` array. See the [payload reference](https://webhookrelay.com/docs/email/docs/email/payload/) for every field. ## [Forward, transform or fan out](#forward-transform-or-fan-out) Because the email becomes an ordinary event on the bucket, you can: - **Forward it** to one or more outputs (your API, an internal service behind a firewall, another webhook host). - **[Transform it first](https://webhookrelay.com/docs/email/docs/webhooks/functions/)** — attach a function to the input or output to reshape the JSON (e.g. pull `text`/`subject` into your own schema, or build a [Slack/Discord/Teams message](https://webhookrelay.com/docs/email/webhook-message-formatter/)). - **Restrict who can send** with a [sender allowlist, and control attachments and rate limits](https://webhookrelay.com/docs/email/docs/email/filtering-and-policy/). ## [Next steps](#next-steps) - [Create & poll email addresses from the CLI](https://webhookrelay.com/docs/email/docs/email/cli/) — do it all from the terminal. - [Email webhook payload reference](https://webhookrelay.com/docs/email/docs/email/payload/) — every field, attachments and headers. - [Sender filtering & policy](https://webhookrelay.com/docs/email/docs/email/filtering-and-policy/) — allowlists, attachments, limits. - [Transform functions](https://webhookrelay.com/docs/email/docs/webhooks/functions/) — reshape the email before delivery. Did this page help you? --- --- title: Webhook Relay - FAQ & Contact | WebhookRelay meta: "og: title": "Webhook Relay - FAQ & Contact" description: Find answers to frequently asked questions (quota, plan change, account deletion, etc.) or contact us if you need further assistance with Webhook Relay. url: https://webhookrelay.com/contact.md file: /contact.md --- # **How can we help?** Need support or have a question about Webhook Relay? We're here to help. Start typing to search... ## **Getting Started** ## Please follow the instructions in the onboarding email you received. If you have any issues, please contact us. [Learn more](https://webhookrelay.com/contact/docs) ## We have a free plan for you to try out. You can also sign up for a paid plan to get more features. We offer a 7-day money-back guarantee. ## Wait for the email to arrive, then click the link to verify your account. If it doesn't arrive, check your spam folder. If you still have issues, please contact us. ## Please follow the instructions in the installation guide. Depending on your use case, you can install the agent via the CLI or Docker. If you still have issues, please contact us. [Learn more](https://webhookrelay.com/contact/docs/installation/cli) ## **Profile & plans** ## Custom enterprise plans are available at more competitive rates than the Pro plan when you need more than 1 million webhooks per month. Please contact us on info@webhookrelay.com for more information. ## You can find your monthly quota by visiting your account details page and clicking on the "buckets" button. It will show you your daily usage and when the quota will be reset. ## If you wish to change your plan, please visit your account details page and click on the "plans" button. ## Please visit your account details page, click on the "plans" button and select the free plan. Your buckets will remain, however if you are above your quota they will get suspended. ## Please visit your account details page, click on the "delete account" button in the bottom of the page (danger zone) ## You can click on the chat icon in the bottom right corner of the page or send an email to info@webhookrelay.com ## **Accounts** ## Please visit your account details page, click on the "change password" button in the bottom of the page (danger zone) ## Please visit your account details page, click on the "change email" button in the bottom of the page (danger zone) ## You cannot change your username unless you contact us. Please send an email to info@webhookrelay.com ## Please send an email to info@webhookrelay.com with your request. ## **Cannot find what you're looking for?** [Contact us](https://webhookrelay.com/contact//mailto:info@webhookrelay.com?subject=Webhook%20Relay%20inquiry) --- --- title: Forward Webhooks to Localhost & Internal Servers meta: "og: title": "Forward Webhooks to Localhost & Internal Servers" description: Forward webhooks from any source to localhost, on-prem servers or your laptop — no public IP required. Inspect, transform, fan out and retry deliveries. url: https://webhookrelay.com/webhooks.md file: /webhooks.md --- # **Webhooks on localhost** Receive webhooks from any source on your localhost. Forward them to your own on-prem endpoints and personal laptops. [Start Forwarding ->](https://my.webhookrelay.com/new-internal-destination) [Learn More](https://webhookrelay.com/webhooks/docs/webhooks/internal/localhost) ![Cron](https://webhookrelay.com/webhooks/images/landing/webhooks/behind-firewall.png) ## **Inspect payloads** When developing locally, inspect payloads and responses in real-time to understand the event structure, what webhooks are being sent and received. ![Cron](https://webhookrelay.com/webhooks/images/landing/webhooks/inspect.png) ### Private and public destinations Configure forwarding to private and public destinations. Send webhooks to both your staging and development environments. ### Broadcasting webhooks Multiple agents can connect to a single bucket. All your team members can connect to a bucket and receive the same webhooks. This allows seamless collaboration and testing. ### Execute serverless functions Webhooks can trigger Functions to customize your workflows. Whether it's accessing 3rd party APIs or inserting into data warehouse, choice is yours. ![Stripes](https://webhookrelay.com/webhooks/images/stripes-dark.svg) ## **Start forwarding webhooks in minutes ** Connect a source, pick a destination, and Webhook Relay handles delivery, retries and transforms. Set up your first webhook in under five minutes. [Start for free ->](https://my.webhookrelay.com/register) Free plan · No credit card required · 7-day money-back guarantee on paid plans --- --- title: Email to Webhook — Inbound Email API | WebhookRelay meta: "og: title": "Email to Webhook — Inbound Email API" description: Turn inbound email into webhooks. Get a unique address; every email is parsed to JSON — sender, subject, text, HTML, attachments — and POSTed to your endpoint. url: https://webhookrelay.com/email-to-webhook.md file: /email-to-webhook.md --- # **Email to Webhook** Turn **inbound email into webhooks**. Get a unique address, and every message you receive is parsed to clean JSON — sender, subject, text, HTML, headers and attachments — and delivered to your endpoint. A developer-friendly **inbound email API** and **email parser**, with no SMTP server to run. [Create an email input](https://my.webhookrelay.com) [Read the docs](https://webhookrelay.com/email-to-webhook/docs/email/) ## **See exactly what your endpoint receives** Pick a sample email — Webhook Relay parses it into the JSON on the right and POSTs it to your endpoint. These are real payload fields, not a mock-up. **Incoming email** From Jon Snow To 5519fb0d-997a-46cd-b102-ac990b4c38fa@in.webhookrelay-mail.com Subject Can't log in to my account Hi, I've been unable to log in since this morning — it just says 'invalid credentials'. Thanks, Jon POST to your endpoint · application/jsonX-Webhookrelay-Source: email ``` { "from": "jon@example.com", "from_name": "Jon Snow", "recipient": "5519fb0d-997a-46cd-b102-ac990b4c38fa@in.webhookrelay-mail.com", "to": [ "5519fb0d-997a-46cd-b102-ac990b4c38fa@in.webhookrelay-mail.com" ], "subject": "Can't log in to my account", "date": "Fri, 26 Jun 2026 11:27:41 +0400", "message_id": "C70BE3E7-4B20-4240-A1B8-2D19E0032FFD@gmail.com", "text": "Hi,\r\n\r\nI've been unable to log in since this morning — it just says 'invalid credentials'.\r\n\r\nThanks,\r\nJon", "headers": { "From": "Jon Snow ", "Subject": "Can't log in to my account", "Return-Path": "" }, "spf": "pass", "dkim": "pass", "dmarc": "pass" } ``` ### **Fields you can use** - `from` / `from_name` — sender address & name - `subject` — the email subject - `text` / `html` — body parts - `to` / `cc` / `recipient` — recipients - `attachments[]` — name, content_type, size, base64 content - `spf` / `dkim` / `dmarc` — auth results - `headers` — every raw header Full [payload reference →](https://webhookrelay.com/email-to-webhook/docs/email/payload/) ### **Reshape it in flight (optional)** Attach a transform function to turn the email into your own schema — or a Slack/Discord/Teams message: ``` const e = JSON.parse(r.body); r.setBody(JSON.stringify({ text: \`📧 ${e.subject} — ${e.from}\` })); ``` Build it visually with the [Slack/Discord/Teams formatter →](https://webhookrelay.com/email-to-webhook/webhook-message-formatter/) ## **How email-to-webhook works** 1. Add an **email input** to a bucket in the dashboard. You get a unique address like `@in.webhookrelay-mail.com`. 2. Point mail at it — from a person, an app, a payment provider, an alert, a report. 3. Webhook Relay parses each message (sender, subject, text, HTML, headers, attachments, SPF/DKIM/DMARC) and **POSTs it as JSON** to your endpoint. 4. Optionally [transform](https://webhookrelay.com/email-to-webhook/docs/webhooks/functions/) it first, [restrict senders](https://webhookrelay.com/email-to-webhook/docs/email/filtering-and-policy/), fan out to several destinations, or deliver to a server [behind a firewall](https://webhookrelay.com/email-to-webhook/webhooks/). ## **What you can build** [Email → Slack](https://webhookrelay.com/email-to-webhook/docs/tutorials/email/slack/) [Email → Discord](https://webhookrelay.com/email-to-webhook/docs/tutorials/email/discord/) [Email → Teams](https://webhookrelay.com/email-to-webhook/docs/tutorials/email/microsoft-teams/) [Email → Google Sheets](https://webhookrelay.com/email-to-webhook/docs/tutorials/email/google-sheets/) [Email → Airtable](https://webhookrelay.com/email-to-webhook/docs/tutorials/email/airtable/) [Email → Notion](https://webhookrelay.com/email-to-webhook/docs/tutorials/email/notion/) [Email → your API](https://webhookrelay.com/email-to-webhook/docs/tutorials/email/api/) [Email → database](https://webhookrelay.com/email-to-webhook/docs/tutorials/email/database/) ## **A simpler inbound-email API** If you've used SendGrid Inbound Parse, Mailgun Routes, CloudMailin or the Zapier Email Parser, this will feel familiar — but Webhook Relay gives you the parsed JSON _and_ the full delivery pipeline in one place: transforms, fan-out to multiple destinations, delivery to localhost or private servers, retries and an audit log. No DNS or MX setup to receive on the shared `in.webhookrelay-mail.com` domain, and a free tier to start. ## **Frequently asked questions**
**How do I turn an email into a webhook?** Add an email input to a bucket in the [Webhook Relay dashboard](https://my.webhookrelay.com). You get a unique address like `@in.webhookrelay-mail.com`; every message sent to it is parsed to JSON and POSTed to your endpoint. See the [docs](https://webhookrelay.com/email-to-webhook/docs/email).
**What does the JSON payload look like?** A flat object with `from`, `from_name`, `subject`, `text`, `html`, `to`, `cc`, `headers`, `spf`/`dkim`/`dmarc` and an `attachments` array (base64 content). See the [payload reference](https://webhookrelay.com/email-to-webhook/docs/email/payload).
**Do I need to set up DNS or an MX record?** No. You receive on the shared `in.webhookrelay-mail.com` domain at the address you are given — nothing to configure. Just send mail to it.
**Can I restrict who can email the address?** Yes. Add an allowlist of `From` addresses to an email input and everything else is silently dropped. You also get per-input rate limiting and SPF/DKIM/DMARC results to check. See [filtering & policy](https://webhookrelay.com/email-to-webhook/docs/email/filtering-and-policy).
**How are attachments delivered?** Inlined into the JSON as base64 in `attachments[].content`, with `name`, `content_type` and `size`. You can drop attachments or cap their total size per message.
**Is it free?** Webhook Relay has a free tier you can start on. Receiving email works the same as any other input, so it uses your normal bucket quota.
## **Related** [Email docs](https://webhookrelay.com/email-to-webhook/docs/email/) [Slack/Discord/Teams Formatter](https://webhookrelay.com/email-to-webhook/webhook-message-formatter/) [Webhook Tester](https://webhookrelay.com/email-to-webhook/webhook-bin/) [Webhook forwarding](https://webhookrelay.com/email-to-webhook/webhooks/) [All guides](https://webhookrelay.com/email-to-webhook/guides/) --- --- title: Webhook to Slack, Discord & Teams Formatter | WebhookRelay meta: "og: title": "Webhook to Slack, Discord & Teams Formatter" description: Free webhook to Slack, Discord and Teams formatter — a Discohook alternative. Paste JSON, template it, build a rich embed and send a live test. No signup. url: https://webhookrelay.com/webhook-message-formatter.md file: /webhook-message-formatter.md --- # **Webhook to Slack, Discord & Teams Formatter** Free online formatter that turns a raw webhook payload into a clean **Slack**, **Discord** or **Microsoft Teams** message. Paste your JSON, template the message with `{{ field }}` placeholders, build a rich embed, then **send a live test** to your Slack, Discord or Teams **incoming webhook** URL — or copy a **Webhook Relay transform function** that does the same conversion automatically, in flight. No signup. **More than a manual message builder.** Tools like Discohook build _one_ Discord message by hand. This turns _any_ provider's webhook — Stripe, GitHub, a CI job, your own app — into a Slack, Discord **or** Microsoft Teams message **automatically**. **Webhook payload (JSON)** **Title / heading** **Message** Use `{{ path.to.field }}` to insert values from the payload above — e.g. `{{ data.order_id }}`. ### **Embed details **(optional) Accent color **Author** **Footer** **Image URL** **Fields** inline inline **Preview** **Webhook Relay**APP **order.created** **🛒 New order #1042** New order from Jon Snow **Amount** 64.95 USD **Status** paid via Webhook Relay **Slack webhook payload** **Send a test to your Slack webhook** Sent server-side through Webhook Relay (no browser CORS errors). Your URL isn't stored. Only Slack, Discord and Teams webhook hosts are accepted. ## **Do it automatically with Webhook Relay** Attach this JavaScript [transform function](https://webhookrelay.com/webhook-message-formatter/features/transform-webhooks/) to a Webhook Relay output and every webhook is reshaped into a Slack message in flight — point any provider at your Webhook Relay URL and it arrives in Slack already formatted. // Webhook Relay transform: format incoming webhooks into a Slack message. // Attach it to the output/destination in Webhook Relay. var input = JSON.parse(r.body || "{}"); function get(obj, path) { var v = String(path).split(".").reduce(function (o, k) { return (o == null) ? undefined : o[k]; }, obj); // Match the live preview: a missing path renders as empty, not "undefined". return (v == null) ? "" : v; } var title = \`🛒 New order #${get(input, "data.order_id")}\`; var message = \`New order from ${get(input, "data.customer")}\`; var author = \`${get(input, "event")}\`; var footer = \`via Webhook Relay\`; var fields = [ { name: \`Amount\`, value: \`${get(input, "data.amount")} ${get(input, "data.currency")}\`, inline: true }, { name: \`Status\`, value: \`${get(input, "data.status")}\`, inline: true } ]; var attachment = { color: "#5865F2", fallback: title, title: title, text: message }; attachment.author_name = author; attachment.fields = fields.map(function (f) { return { title: f.name, value: f.value, short: f.inline }; }); attachment.footer = footer; var payload = { text: title, attachments: [attachment] }; r.setBody(JSON.stringify(payload)); r.setHeader("Content-Type", "application/json"); Creates a bucket with this function on your account that forwards incoming webhooks to your Slack URL. Free — sign in or sign up in one step. Prefer to wire it yourself? See [webhook to Slack](https://webhookrelay.com/webhook-message-formatter/blog/webhook-to-slack/), [Discord](https://webhookrelay.com/webhook-message-formatter/blog/webhook-to-discord/) or [Microsoft Teams](https://webhookrelay.com/webhook-message-formatter/blog/webhook-to-microsoft-teams/) for the full setup. ## **FAQ**
**How do I format a message for a Slack or Teams incoming webhook?** Slack, Microsoft Teams and Discord each expose an incoming webhook URL that accepts a specific JSON message shape. Pick the platform, paste your source payload, template the title and message with {{ field }} placeholders, and this tool builds the exact incoming-webhook JSON to POST — Slack attachments/blocks, a Teams MessageCard, or a Discord embed — then hit “Send test” to post it live.
**Is this a Discohook alternative?** Yes, and it goes further. Discohook hand-builds one Discord message; this is a Discohook alternative that maps any incoming webhook — Stripe, GitHub, a CI job, your own app — into a Slack, Discord or Teams message automatically, not just a one-off you paste by hand.
**How is this different from Discohook?** Discohook is a manual composer for a single Discord message. This formatter maps an incoming webhook payload from any provider into a Slack, Discord or Microsoft Teams message with {{ field }} templates — and can run that same conversion automatically on your live webhooks via a Webhook Relay transform function.
**How do I send a webhook to Slack, Discord or Teams?** Each platform accepts an incoming webhook URL that expects a specific JSON shape. Paste your source payload here, template the message, build the embed, then either paste your webhook URL and hit “Send test”, or attach the generated Webhook Relay transform function to convert live webhooks automatically.
**What is the {{ field }} syntax?** It is a placeholder that pulls a value out of the JSON payload by dotted path. {{ data.order_id }} reads the order_id inside the data object. It works in the title, message, author, footer, image URL and every field. Anything not wrapped in braces is used literally.
**Is “Send test” safe — do you store my webhook URL?** No. The test send is proxied server-side through Webhook Relay only so your browser avoids CORS errors; the URL and payload are forwarded once and not stored. Only Slack, Discord and Microsoft Teams webhook hosts are accepted, and the endpoint is rate limited per IP.
**Do I need an account?** No — the formatter, preview and test send are completely free and need no signup. To run the conversion automatically on your real webhooks, create a free Webhook Relay account and attach the transform function to an output.
## **Related free tools** [Webhook Tester (Bin)](https://webhookrelay.com/webhook-message-formatter/webhook-bin/) [HMAC Signature Verifier](https://webhookrelay.com/webhook-message-formatter/hmac-verification/) [CloudEvents Validator](https://webhookrelay.com/webhook-message-formatter/cloudevents-validator/) [Transform Webhooks](https://webhookrelay.com/webhook-message-formatter/features/transform-webhooks/) [All Guides](https://webhookrelay.com/webhook-message-formatter/guides/) --- --- title: Free URL Redirect Generator — 301 & 302 | WebhookRelay meta: "og: title": "Free URL Redirect Generator — 301 & 302" description: Free URL redirect generator. Create an instant 301, 302, 303, 307 or 308 redirect endpoint with no signup — get a public URL that forwards every request. url: https://webhookrelay.com/url-redirect-generator.md file: /url-redirect-generator.md --- # **Free URL Redirect Generator** Create a **URL redirect** in seconds — no signup. Enter a destination, pick the redirect type (**301**, **302**, 303, 307 or 308), and you get a public endpoint URL. Anything that hits it — a browser, a webhook sender, a link — is instantly forwarded to your target with a real HTTP `Location` redirect, and every request is captured so you can see exactly what came in. **Redirect to (destination URL) ** **Redirect type ** Free and instant. The redirect endpoint stays active for 48 hours of inactivity — for a permanent, authenticated redirect use a [Webhook Relay](https://webhookrelay.com/url-redirect-generator/) account. Need the reverse — capture and inspect incoming webhooks without redirecting? Use the free [Webhook Tester (Bin)](https://webhookrelay.com/url-redirect-generator/webhook-bin/). Forwarding webhooks between servers or to localhost? See [webhook forwarding](https://webhookrelay.com/url-redirect-generator/features/forwarding-rules/). ## **What is a URL redirect?** A **URL redirect** (also called URL forwarding) is an HTTP response that tells the client — usually a browser, but it can be any HTTP client — to go somewhere else. Instead of returning content, the server replies with a **3xx status code** and a `Location` header pointing at the new address. The client then automatically requests that address. It's how `http://example.com` ends up at `https://www.example.com`, how a moved page sends visitors to its new home, and how a short or vanity link expands to its real destination. The generator above creates a real endpoint that does exactly this. You don't need a server, DNS changes or a deploy — you get a public URL immediately, and you choose whether the redirect is temporary or permanent and whether it preserves the HTTP method and body. Because the endpoint is a [Webhook Bin](https://webhookrelay.com/url-redirect-generator/webhook-bin/), it also records every request that passes through, which makes it handy for debugging clients, link tracking and migrations. ## **301 vs 302 vs 307 vs 308 — which redirect should I use?** The status code you pick changes how clients, browsers and search engines treat the redirect. The two things that differ are whether the move is _permanent_ (cached, and link equity passed on) and whether the original HTTP _method and body_ are preserved. | **Code** | **Name** | **Permanent?** | **Keeps method** | **Use it for** | | --- | --- | --- | --- | --- | | `301` | Moved Permanently | Yes | No (→ GET) | A page or URL has permanently moved; pass SEO ranking to the new URL. | | `302` | Found | No | No (→ GET) | Temporary detour — maintenance, A/B tests, geo or device routing. | | `303` | See Other | No | No (→ GET) | After a POST, send the client to a result page with a GET. | | `307` | Temporary Redirect | No | Yes | Temporary move that must preserve POST/PUT method and body — APIs, webhooks. | | `308` | Permanent Redirect | Yes | Yes | Permanent move that must preserve the method and body — API/webhook endpoints. | Rule of thumb: use **301** when a page has permanently moved and you want search engines to pass ranking to the new URL; use **302** for a temporary detour (maintenance, A/B tests, geo-routing). If the original request was a `POST`, `PUT` or `DELETE` and you need the method and body to survive the redirect — as you do when forwarding a webhook — choose **307** (temporary) or **308** (permanent). A classic `301`/`302` will turn the follow-up request into a `GET` and drop the body, which silently breaks API and webhook traffic. ## **What you can do with it** - **Forward a deprecated webhook URL** to its replacement with a 307/308 so the method and JSON body are preserved. - **Point an old or vanity link** at its new destination without standing up a server or touching DNS. - **Test how a client follows redirects** — does your HTTP library follow 307? Does it re-POST? Watch the captured request to find out. - **Migrate an endpoint gradually**: send traffic to the new host while the captured log shows you exactly who's still calling the old one. - **Share a single stable link** in a doc, email or QR code and change where it points later by editing the bin. ## **How it works** When you create a redirect, we provision a Webhook Bin and configure its response to redirect. Each incoming request is logged and then answered with your chosen status and a `Location` header: ``` $ curl -i https://bin.webhookrelay.com/v1/webhooks/ HTTP/1.1 302 Found Location: https://example.com/destination Content-Length: 0 ``` A browser follows that header automatically and lands on the destination; `curl -L` or any HTTP client that follows redirects does the same. Meanwhile the request is captured in the [bin](https://webhookrelay.com/url-redirect-generator/webhook-bin/), so you can inspect the method, headers, query string, body and client IP of everything that hit the link. ## **URL redirect vs URL shortener** A URL shortener is one specific use of redirects: it maps a short, branded slug to a long destination, usually with click analytics. This generator is lower-level and more flexible — you control the exact status code (301/302/303/307/308), it preserves the request method and body when you need it to (so it works for API and webhook traffic, not just links), and it captures the full request for inspection. If all you want is a tidy short link, a shortener is simpler; if you're debugging clients, forwarding webhooks or testing redirect behaviour, this gives you the control and visibility a shortener won't. ## **Frequently asked questions**
**How do I create a URL redirect?** Enter the destination URL above, choose a redirect type (302 temporary by default, or 301 permanent), and click **Create redirect URL**. You get a public endpoint immediately — every request to it is forwarded to your destination with an HTTP `Location` redirect. No signup, server or DNS change required.
**What is the difference between a 301 and a 302 redirect?** A **301** is permanent: clients and search engines cache it and pass link equity to the new URL. A **302** is temporary: clients keep using the original URL and ranking is not transferred. Use 301 for permanent moves and 302 for temporary detours.
**Which redirect keeps the POST method and body?** Use **307** (temporary) or **308** (permanent). Unlike 301/302 — which most clients turn into a `GET` and strip the body — 307 and 308 make the client repeat the original method and body, so they are correct for redirecting API calls or [forwarding webhooks](https://webhookrelay.com/url-redirect-generator/webhook-bin).
**Is the URL redirect generator free?** Yes, completely free and no signup. The endpoint is a [Webhook Bin](https://webhookrelay.com/url-redirect-generator/webhook-bin) that stays active for 48 hours of inactivity. For a permanent, authenticated redirect with custom domains, create a [Webhook Relay](https://webhookrelay.com/url-redirect-generator/) account.
**Can I see who hits the redirect link?** Yes. Because the endpoint is a Webhook Bin, every request is captured before the redirect is returned. Open **Inspect captured requests** to see the method, headers, query string, body and client IP of everything that follows the link.
**Does the redirect work for webhooks and API calls, not just browsers?** Yes. Any HTTP client that follows redirects will be forwarded. For `POST`/`PUT`/`DELETE` traffic such as webhooks, choose 307 or 308 so the method and request body are preserved across the redirect.
## **Related free tools** [Webhook Tester (Bin)](https://webhookrelay.com/url-redirect-generator/webhook-bin/) [Slack/Discord Formatter](https://webhookrelay.com/url-redirect-generator/webhook-message-formatter/) [HMAC Signature Verifier](https://webhookrelay.com/url-redirect-generator/hmac-verification/) [CloudEvents Validator](https://webhookrelay.com/url-redirect-generator/cloudevents-validator/) [Verify Webhook Signatures](https://webhookrelay.com/url-redirect-generator/verify-webhook-signature/) --- --- title: How to receive Stripe webhooks on localhost | WebhookRelay meta: "og: title": "How to receive Stripe webhooks on localhost" description: Often when building an application that integrates with 3rd party services we need a way to receive webhooks url: https://webhookrelay.com/blog/receiving-stripe-webhooks-localhost.md file: /blog/receiving-stripe-webhooks-localhost.md --- ![Stripes](https://webhookrelay.com/blog/receiving-stripe-webhooks-localhost/images/stripes.svg) # **How to receive Stripe webhooks on localhost** Often when building an application that integrates with 3rd party services we need a way to receive webhooks ![Receiving Stripe webhooks on localhost with Webhook Relay](https://webhookrelay.com/blog/receiving-stripe-webhooks-localhost/images/blog/stripe-webhooks-localhost/stripe-webhooks-hero.png) In recent years [Stripe](https://stripe.com/) has become a major payments provider. It is loved by managers and developers for a reason. Stripe has easy to use APIs, SDKs in multiple languages and outstanding documentation. Like other payment platforms, Stripe utilizes webhooks to inform about customer, subscription, card and many other changes in the state. While this strategy works great in production, during development it can be tricky to receive these webhooks, especially when webhooks are critical for building subscription (or recurring payment) based systems where your backend system needs to track subscription status. In this article, we will: - Build a simple application to handle several Stripe webhooks that indicate subscription change. - We will use `relay forward` command to receive webhooks on localhost. - We will use Stripe's webhooks testings dashboard to simulate subscription change events. ## [Prerequisites](#prerequisites) This post/guide assumes that you have: - Stripe account. You can register at [https://dashboard.stripe.com/register](https://dashboard.stripe.com/register). - Webhook Relay account. You can register at [https://my.webhookrelay.com/register](https://my.webhookrelay.com/register). - Relay CLI, installation instructions can be found [here](https://docs.webhookrelay.com/installation-options/installation-options/install-cli). - Golang environment, installation instructions can be found here: [https://golang.org/doc/install](https://golang.org/doc/install). ## [Building application](#building-application) There are many libraries available for Stripe. You can find a maintained list on Stripe's documentation page here: [https://stripe.com/docs/libraries](https://stripe.com/docs/libraries). Our sample app is written in Go. Application source is pretty straightforward. There is only one handler to receive webhooks (documentation on how to use webhooks is here: [https://stripe.com/docs/webhooks](https://stripe.com/docs/webhooks)), validate signature and print to the terminal customer ID and current subscription status. If signature validation gives you trouble, our [Stripe signature verifier](https://webhookrelay.com/blog/receiving-stripe-webhooks-localhost/verify-stripe-webhook-signature/) recomputes the `Stripe-Signature` digest from a payload and secret so you can compare against what your code produces: ``` package main import ( "fmt" "io/ioutil" "log" "net/http" "os" stripe "github.com/stripe/stripe-go" "github.com/stripe/stripe-go/webhook" ) // port - default port to start application on const port = ":8090" // webhook event types const ( stripeEventTypeSubscriptionUpdated string = "customer.subscription.updated" // canceled subscription stripeEventTypeSubscriptionDeleted string = "customer.subscription.deleted" // card deletion event stripeEventTypeSourceDeleted string = "customer.source.deleted" ) func validateSignature(payload []byte, header, secret string) (stripe.Event, error) { return webhook.ConstructEvent(payload, header, secret) } func main() { secret := os.Getenv("SIGNING_SECRET") if secret == "" { fmt.Println("SIGNING_SECRET env variable is required") os.Exit(1) } // preparing HTTP server srv := &http.Server{Addr: port, Handler: http.DefaultServeMux} // incoming stripe webhook handler http.HandleFunc("/stripe", func(resp http.ResponseWriter, req *http.Request) { body, err := ioutil.ReadAll(req.Body) if err != nil { resp.WriteHeader(http.StatusBadRequest) return } // validating signature event, err := validateSignature(body, req.Header.Get("Stripe-Signature"), secret) if err != nil { resp.WriteHeader(http.StatusBadRequest) fmt.Printf("Failed to validate signature: %s", err) return } switch event.Type { case stripeEventTypeSubscriptionUpdated, stripeEventTypeSubscriptionDeleted: // subscription status change customerID, ok := event.Data.Obj["customer"].(string) if !ok { fmt.Println("customer key missing from event.Data.Obj") return } subStatus, ok := event.Data.Obj["status"].(string) if !ok { fmt.Println("status key missing from event.Data.Obj") return } fmt.Printf("customer %s subscription updated, current status: %s \n", customerID, subStatus) case stripeEventTypeSourceDeleted: customerID, ok := event.Data.Obj["customer"].(string) if !ok { fmt.Println("customer key missing from event.Data.Obj") return } fmt.Printf("card deleted for customer %s \n", customerID) } }) fmt.Printf("Receiving Stripe webhooks on http://localhost%s/stripe \n", port) // starting server err := srv.ListenAndServe() if err != http.ErrServerClosed { log.Fatalf("listen: %s\n", err) } } ``` Source code can be found here: [https://github.com/webhookrelay/stripe-webhook-demo](https://github.com/webhookrelay/stripe-webhook-demo). To install and run it, in your Go working environment you can just do: ``` go get github.com/webhookrelay/stripe-webhook-demo cd $GOPATH/src/github.com/webhookrelay/stripe-webhook-demo/ go install ``` Our application will expect Stripe webhooks on [http://localhost:8090/stripe](http://localhost:8090/stripe). ## [Receiving webhooks on localhost](#receiving-webhooks-on-localhost) To start receiving webhooks on localhost, we will use **relay** CLI: ``` relay forward --bucket stripe http://localhost:8090/stripe ``` flag _--bucket stripe_ is optional but helps a lot when we restart relay CLI as it reuses the same public endpoint. Output of the command should display your _my.webhookrelay.com/v1/webhooks/{id here}_ public endpoint: ``` relay forward --bucket stripe http://localhost:8090/stripe Forwarding: https://my.webhookrelay.com/v1/webhooks/d52caf28-d7ce-1e90-b9e3-36294f1dca74 -> http://localhost:8090/stripe ``` ## [Testing webhooks via Stripe](#testing-webhooks-via-stripe) ![getting webhooks on localhost](https://webhookrelay.com/blog/receiving-stripe-webhooks-localhost/images/blog/stripe-webhooks-localhost/relay-forward-stripe.gif) Let's go to our Stripe dashboard, API webhooks section ([https://dashboard.stripe.com/account/webhooks](https://dashboard.stripe.com/account/webhooks)) and: 1. Add an endpoint with your unique Webhook Relay URL. 2. Get a signing secret, set it for our stripe-webhook-demo application, and launch it:``` $ export SIGNING_SECRET=whsec_******************************** $ stripe-webhook-demo Receiving Stripe webhooks on http://localhost:8090/stripe ``` 3. Click on "Send test webhook", select `customer.subscription.updated` and send it. 4. View the stripe-webhook-demo output. It should display a customer ID and subscription status:``` $ stripe-webhook-demo Receiving Stripe webhooks on http://localhost:8090/stripe customer cus_00000000000000 subscription updated, current status: active ``` ## [Wrapping up](#wrapping-up) In this post we created a sample application that can receive webhooks from Stripe. We used `relay forward` command to receive webhooks on localhost and checked out Stripe's webhook testing dashboard to speed up development. --- --- title: What Is a Webhook? Definition, Examples & How They Work meta: "og: title": "What Is a Webhook? Definition, Examples & How They Work" description: A webhook is an automated HTTP request a service sends when an event happens. How webhooks work, examples, and how to set one up in Node.js. url: https://webhookrelay.com/blog/what-is-webhook.md file: /blog/what-is-webhook.md --- ![Stripes](https://webhookrelay.com/blog/what-is-webhook/images/stripes.svg) # **What is a Webhook? Definition, Examples & How to Set One Up** A webhook is an automated HTTP request a service sends when an event happens. How webhooks work, examples, and how to set one up in Node.js. A **webhook** is an automated HTTP request that one system sends to a URL you control whenever a specific event happens. Instead of your application constantly asking another service "has anything changed yet?", the service simply **pushes** the information to you the instant it does. For technical folks, it's easiest to think of webhooks as **"reverse APIs"** or **"push APIs"**: one server makes an API request to another server. (For a deeper comparison, see [webhooks vs API](https://webhookrelay.com/blog/what-is-webhook/blog/webhooks-vs-api/).) If you prefer a plain-language version: imagine a friend who bakes cakes. Rather than you checking the kitchen every five minutes, your friend texts you the moment a cake is ready. A webhook is that text message between web services. ## [How do webhooks work?](#how-do-webhooks-work) Every webhook integration follows the same four steps: 1. **You expose a URL** that accepts `POST` requests — an ordinary route in your app, like `https://example.com/webhooks/stripe`. 2. **You register that URL** with the provider and choose which events you care about (Stripe → Developers → Webhooks, GitHub → Settings → Webhooks, and so on). 3. **The provider POSTs to your URL** each time one of those events happens, sending a JSON payload plus headers describing the event and proving it's authentic. 4. **Your handler acknowledges** with a `2xx` status. If you don't — or you're too slow — most providers treat it as a failure and retry later. Here's what actually arrives on the wire when a payment succeeds: ``` POST /webhooks/stripe HTTP/1.1 Host: example.com Content-Type: application/json Stripe-Signature: t=1754640000,v1=5257a869e7ecebeda32affa62cdca3fa... { "id": "evt_1PgkT2xQ", "type": "checkout.session.completed", "created": 1754640000, "data": { "object": { "id": "cs_test_a1b2c3", "amount_total": 4900, "currency": "usd", "customer_email": "ada@example.com" } } } ``` That's the whole mechanism — a POST, a JSON body, and a signature header. Everything else is a detail of the specific provider. Curious which providers generate the most real-world webhook traffic, and what those payloads typically look like? We publish live figures from our own ingesters on [the State of Webhooks](https://webhookrelay.com/blog/what-is-webhook/state-of-webhooks/). ## [What are examples of webhooks?](#what-are-examples-of-webhooks) The most common real-world webhook examples: 1. **Payments** — when a payment is made, a webhook is sent to the merchant's server so it can process the order. See [receiving Stripe webhooks on localhost](https://webhookrelay.com/blog/what-is-webhook/blog/receiving-stripe-webhooks-localhost/). 2. **Software testing / CI/CD** — when code is pushed to GitHub, a webhook notifies a CI system to run tests and deploy. See [the GitHub + Jenkins guide](https://webhookrelay.com/blog/what-is-webhook/blog/github-jenkins-guide/). 3. **Connecting services together** — when you want to glue two third-party tools together, webhooks are the bridge. See [Google Home + IFTTT + Node-RED](https://webhookrelay.com/blog/what-is-webhook/blog/google-home-ifttt-node-red/). 4. **Notifications** — posting a message to a [Discord](https://webhookrelay.com/blog/what-is-webhook/blog/webhook-to-discord/) or [Slack](https://webhookrelay.com/blog/what-is-webhook/blog/webhook-to-slack/) channel when something happens, like a [TradingView alert](https://webhookrelay.com/blog/what-is-webhook/blog/trading-view/). ## [Anatomy of a webhook request](#anatomy-of-a-webhook-request) Every webhook is an ordinary HTTP request, so it has three parts you'll work with: - **The method and URL** — almost always a `POST` to the endpoint URL you registered with the provider. - **Headers** — metadata about the delivery. The important ones are the `Content-Type` (usually `application/json`), an event-type header (e.g. GitHub's `X-GitHub-Event`), and a **signature** header the provider uses to prove the request is authentic (e.g. `Stripe-Signature`, `X-Hub-Signature-256`). - **The body (payload)** — the JSON describing what happened: the event type, an ID, a timestamp, and the data object. Knowing the exact shape of these three parts is the whole job when you integrate a new provider — which is why it helps to [capture a real request and inspect it](https://webhookrelay.com/blog/what-is-webhook/webhook-bin/) — a free [webhook.site alternative](https://webhookrelay.com/blog/what-is-webhook/blog/webhook-site-alternative/) — before writing any handler code. ## [Webhooks vs APIs vs polling](#webhooks-vs-apis-vs-polling) | Approach | Direction | When data moves | Best for | | --- | --- | --- | --- | | **REST API** | You → provider (pull) | When you ask | On-demand reads and writes | | **Polling** | You → provider (pull, on a timer) | Every N seconds, whether or not anything changed | Simple, but wasteful and slow | | **Webhook** | Provider → you (push) | The instant an event happens | Real-time reactions to events | Webhooks replace polling: instead of asking "has anything changed?" on a loop, the provider tells you the moment it does. See the full breakdown in [webhooks vs API](https://webhookrelay.com/blog/what-is-webhook/blog/webhooks-vs-api/), and how they differ from a persistent connection in [webhook vs WebSocket](https://webhookrelay.com/blog/what-is-webhook/blog/webhook-vs-websocket/). ## [How to receive a webhook in an application](#how-to-receive-a-webhook-in-an-application) A webhook is, at its core, just a **POST request to a URL on your server**. Instead of a person visiting your site or a CLI calling your API, it's another website POSTing data to you. So to receive one, you need a server listening for POST requests on a specific path. Here it is in Node.js with [Express](https://expressjs.com/en/starter/installing.html): ``` npm init -y npm install express ``` Create `index.js`: ``` const express = require('express') const app = express() app.use(express.json()) const port = 3000 app.post('/webhooks', (req, res) => { console.log('Received webhook:', req.body) res.sendStatus(200) // always acknowledge quickly }) app.listen(port, () => { console.log(\`Listening for webhooks on port ${port}\`) }) ``` Start it: ``` node index.js ``` And send a test webhook with `curl`: ``` curl -X POST --data '{"important":"data"}' \ -H 'Content-Type: application/json' \ http://localhost:3000/webhooks ``` That's about as complicated as it gets — you can now receive webhooks from any service and process them. ## [How to set up a webhook (and receive it on localhost)](#how-to-set-up-a-webhook-and-receive-it-on-localhost) Setting up a webhook is two steps: 1. **Expose a public URL** that listens for POST requests (the handler above). 2. **Register that URL** in the sending service's webhook settings (e.g. Stripe → Developers → Webhooks, or GitHub → Settings → Webhooks). The tricky part is step 1 during development: providers can only call **public** URLs, but your handler runs on `localhost`. That's what Webhook Relay solves — [inspect the incoming payload in your browser](https://webhookrelay.com/blog/what-is-webhook/webhook-bin/), then forward it to your machine with the agent: ``` relay forward --bucket my-app http://localhost:3000/webhooks ``` No public IP, no firewall changes, and the URL stays the same so you configure the provider once. See [how to test webhooks](https://webhookrelay.com/blog/what-is-webhook/blog/how-to-test-webhooks/) for the full workflow. ## [Are webhooks secure?](#are-webhooks-secure) A webhook endpoint is a **public URL** — anyone who discovers it can POST to it. On its own that's a real risk: a forged `payment.succeeded` event could have your app ship an order nobody paid for. Webhooks become secure when you verify that each request genuinely came from the provider. Almost all of them sign the payload with an HMAC computed over the **raw request body** using a shared secret, delivered in a header such as `Stripe-Signature` or `X-Hub-Signature-256`. Your handler recomputes that HMAC and compares it before trusting anything. Four rules cover most of it: - **Verify against the raw bytes.** Parsing JSON and re-serializing it changes the body, and every signature check will fail. - **Compare in constant time** (`crypto.timingSafeEqual` in Node) so you don't leak the secret through timing. - **Check the timestamp** where the provider sends one, so an old captured request can't be replayed. - **Always use HTTPS**, and treat the secret like any other credential. You can test a signature against your secret right in the browser with the [webhook signature verifier](https://webhookrelay.com/blog/what-is-webhook/verify-webhook-signature/), including provider-specific pages for [Stripe](https://webhookrelay.com/blog/what-is-webhook/verify-stripe-webhook-signature/), [GitHub](https://webhookrelay.com/blog/what-is-webhook/verify-github-webhook-signature/), [Shopify](https://webhookrelay.com/blog/what-is-webhook/verify-shopify-webhook-signature/), [Slack](https://webhookrelay.com/blog/what-is-webhook/verify-slack-webhook-signature/) and [Twilio](https://webhookrelay.com/blog/what-is-webhook/verify-twilio-webhook-signature/). ## [Webhook best practices](#webhook-best-practices) - **Always respond `2xx` quickly.** If you return an error or respond slowly, the sender will retry. Do heavy work asynchronously and acknowledge first. Only return `5xx` when you genuinely can't process the event (e.g. the database is down) and want a retry. - **Verify the signature.** Most providers sign webhooks with an HMAC so you can confirm authenticity. See [how to verify a webhook signature](https://webhookrelay.com/blog/what-is-webhook/blog/verify-webhook-signature/) and the free [HMAC verifier](https://webhookrelay.com/blog/what-is-webhook/hmac-verification/). - **Don't rely on ordering.** Webhooks can arrive out of order. If order matters, use a queue or sort by a timestamp in the payload. - **Make handlers idempotent.** The same event can be delivered more than once; de-duplicate on an event ID so reprocessing is safe. - **Keep payloads thin.** If you're sending webhooks, send identifiers and let the receiver fetch full data via your API. This avoids stale data and keeps payloads small. For the full checklist, see [webhook security best practices](https://webhookrelay.com/blog/what-is-webhook/blog/webhook-security/), [webhook authentication methods](https://webhookrelay.com/blog/what-is-webhook/blog/webhook-authentication/) and [retries & idempotency](https://webhookrelay.com/blog/what-is-webhook/blog/webhook-retries-and-idempotency/). ## [How to test and debug webhooks](#how-to-test-and-debug-webhooks) You can't see a webhook until something sends one, so the fastest way to learn a provider's payload is to capture a real request: 1. **Capture & inspect** — point the provider at a free [webhook tester](https://webhookrelay.com/blog/what-is-webhook/webhook-bin/) URL and read the exact headers and body. 2. **Verify the signature** — confirm authenticity with the [HMAC verifier](https://webhookrelay.com/blog/what-is-webhook/hmac-verification/) and the [verify a webhook signature](https://webhookrelay.com/blog/what-is-webhook/blog/verify-webhook-signature/) guide. 3. **Forward to localhost** — drive your local handler with real events using the relay agent. See [how to test webhooks](https://webhookrelay.com/blog/what-is-webhook/blog/how-to-test-webhooks/) and [how to debug webhooks](https://webhookrelay.com/blog/what-is-webhook/blog/how-to-debug-webhooks/). ## [Webhooks by provider](#webhooks-by-provider) Most integrations follow the same pattern with provider-specific signatures and event shapes. These guides cover the exact details for popular services — capture and inspect the payload, then receive it on localhost: - **Payments:** [Stripe](https://webhookrelay.com/blog/what-is-webhook/blog/receiving-stripe-webhooks-localhost/), [PayPal](https://webhookrelay.com/blog/what-is-webhook/blog/receiving-paypal-webhooks-localhost/), [Square](https://webhookrelay.com/blog/what-is-webhook/blog/receive-square-webhooks-locally/), [Adyen](https://webhookrelay.com/blog/what-is-webhook/blog/receive-adyen-webhooks-locally/), [Coinbase Commerce](https://webhookrelay.com/blog/what-is-webhook/blog/receive-coinbase-commerce-webhooks-locally/), [Razorpay](https://webhookrelay.com/blog/what-is-webhook/blog/receive-razorpay-webhooks-locally/), [Mollie](https://webhookrelay.com/blog/what-is-webhook/blog/receive-mollie-webhooks-locally/), [Plaid](https://webhookrelay.com/blog/what-is-webhook/blog/receive-plaid-webhooks-locally/) - **Developer tools:** [GitHub](https://webhookrelay.com/blog/what-is-webhook/blog/receive-github-webhooks-locally/), [GitLab](https://webhookrelay.com/blog/what-is-webhook/blog/receive-gitlab-webhooks-locally/), [Vercel](https://webhookrelay.com/blog/what-is-webhook/blog/receive-vercel-webhooks-locally/), [Sentry](https://webhookrelay.com/blog/what-is-webhook/blog/receive-sentry-webhooks-locally/), [Linear](https://webhookrelay.com/blog/what-is-webhook/blog/receive-linear-webhooks-locally/), [Jira](https://webhookrelay.com/blog/what-is-webhook/blog/receive-jira-webhooks-locally/), [Webflow](https://webhookrelay.com/blog/what-is-webhook/blog/receive-webflow-webhooks-locally/) - **Messaging & forms:** [Twilio](https://webhookrelay.com/blog/what-is-webhook/blog/receive-twilio-webhooks-locally/), [Slack](https://webhookrelay.com/blog/what-is-webhook/blog/receive-slack-events-locally/), [SendGrid](https://webhookrelay.com/blog/what-is-webhook/blog/receive-sendgrid-webhooks-locally/), [Mailgun](https://webhookrelay.com/blog/what-is-webhook/blog/receive-mailgun-webhooks-locally/), [Mailchimp](https://webhookrelay.com/blog/what-is-webhook/blog/receive-mailchimp-webhooks-locally/), [Typeform](https://webhookrelay.com/blog/what-is-webhook/blog/receive-typeform-webhooks-locally/), [Calendly](https://webhookrelay.com/blog/what-is-webhook/blog/receive-calendly-webhooks-locally/) - **SaaS, auth & storage:** [Shopify](https://webhookrelay.com/blog/what-is-webhook/blog/receiving-shopify-webhooks-flask-api/), [HubSpot](https://webhookrelay.com/blog/what-is-webhook/blog/receive-hubspot-webhooks-locally/), [Intercom](https://webhookrelay.com/blog/what-is-webhook/blog/receive-intercom-webhooks-locally/), [Clerk](https://webhookrelay.com/blog/what-is-webhook/blog/receive-clerk-webhooks-locally/), [Auth0](https://webhookrelay.com/blog/what-is-webhook/blog/receive-auth0-webhooks-locally/), [Okta](https://webhookrelay.com/blog/what-is-webhook/blog/receive-okta-webhooks-locally/), [WorkOS](https://webhookrelay.com/blog/what-is-webhook/blog/receive-workos-webhooks-locally/), [DocuSign](https://webhookrelay.com/blog/what-is-webhook/blog/receive-docusign-webhooks-locally/), [Zendesk](https://webhookrelay.com/blog/what-is-webhook/blog/receive-zendesk-webhooks-locally/), [Asana](https://webhookrelay.com/blog/what-is-webhook/blog/receive-asana-webhooks-locally/), [Box](https://webhookrelay.com/blog/what-is-webhook/blog/receive-box-webhooks-locally/), [Dropbox](https://webhookrelay.com/blog/what-is-webhook/blog/receive-dropbox-webhooks-locally/), [Zoom](https://webhookrelay.com/blog/what-is-webhook/blog/receive-zoom-webhooks-locally/), [Datadog](https://webhookrelay.com/blog/what-is-webhook/blog/receive-datadog-webhooks-locally/) Looking for a no-code option? Every provider also has a dedicated tester — for example the [Stripe webhook tester](https://webhookrelay.com/blog/what-is-webhook/blog/stripe-webhook-tester/) and [GitHub webhook tester](https://webhookrelay.com/blog/what-is-webhook/blog/github-webhook-tester/). ## [Routing and transforming webhooks](#routing-and-transforming-webhooks) Once you receive a webhook you often want to send it somewhere else, or reshape it first. Webhook Relay can [transform payloads](https://webhookrelay.com/blog/what-is-webhook/features/transform-webhooks/) and fan a single event out to [multiple destinations](https://webhookrelay.com/blog/what-is-webhook/features/webhook-multiple-destinations/): - [Webhook to Slack](https://webhookrelay.com/blog/what-is-webhook/blog/webhook-to-slack/), [Discord](https://webhookrelay.com/blog/what-is-webhook/blog/webhook-to-discord/), [Microsoft Teams](https://webhookrelay.com/blog/what-is-webhook/blog/webhook-to-microsoft-teams/) and [Telegram](https://webhookrelay.com/blog/what-is-webhook/blog/webhook-to-telegram/) - [Webhook to Notion](https://webhookrelay.com/blog/what-is-webhook/blog/webhook-to-notion/), [Airtable](https://webhookrelay.com/blog/what-is-webhook/blog/webhook-to-airtable/), [Google Sheets](https://webhookrelay.com/blog/what-is-webhook/blog/webhook-to-google-sheets/) and [Email](https://webhookrelay.com/blog/what-is-webhook/blog/webhook-to-email/) - [Webhook to HubSpot](https://webhookrelay.com/blog/what-is-webhook/blog/webhook-to-hubspot/), [Salesforce](https://webhookrelay.com/blog/what-is-webhook/blog/webhook-to-salesforce/), [Jira](https://webhookrelay.com/blog/what-is-webhook/blog/webhook-to-jira/) and [Trello](https://webhookrelay.com/blog/what-is-webhook/blog/webhook-to-trello/) - [Webhook to PagerDuty](https://webhookrelay.com/blog/what-is-webhook/blog/webhook-to-pagerduty/), [Opsgenie](https://webhookrelay.com/blog/what-is-webhook/blog/webhook-to-opsgenie/), [Datadog](https://webhookrelay.com/blog/what-is-webhook/blog/webhook-to-datadog/) and [Mattermost](https://webhookrelay.com/blog/what-is-webhook/blog/webhook-to-mattermost/) - [Webhook to BigQuery](https://webhookrelay.com/blog/what-is-webhook/blog/webhook-to-bigquery/), [AWS SQS](https://webhookrelay.com/blog/what-is-webhook/blog/webhook-to-sqs/), [S3](https://webhookrelay.com/blog/what-is-webhook/blog/webhook-to-s3/) and [GCS](https://webhookrelay.com/blog/what-is-webhook/blog/webhook-to-gcs/) Setting up the destination first? Here's how to get a [Slack webhook URL](https://webhookrelay.com/blog/what-is-webhook/blog/how-to-get-slack-webhook-url/), a [Discord webhook URL](https://webhookrelay.com/blog/what-is-webhook/blog/how-to-get-discord-webhook-url/), a [Microsoft Teams webhook](https://webhookrelay.com/blog/what-is-webhook/blog/microsoft-teams-webhook/), or how to [send a webhook from Google Forms](https://webhookrelay.com/blog/what-is-webhook/blog/google-forms-webhook/). ## [Next steps](#next-steps) - [How to test webhooks](https://webhookrelay.com/blog/what-is-webhook/blog/how-to-test-webhooks/) — inspect, forward to localhost, and replay. - [Webhooks vs API](https://webhookrelay.com/blog/what-is-webhook/blog/webhooks-vs-api/) — when to push vs pull. - [Webhook vs WebSocket](https://webhookrelay.com/blog/what-is-webhook/blog/webhook-vs-websocket/) — one-off callbacks vs a persistent connection. - [Webhook architecture diagram](https://webhookrelay.com/blog/what-is-webhook/blog/webhook-architecture-diagram/) — how the pieces fit end to end. - [Webhook security best practices](https://webhookrelay.com/blog/what-is-webhook/blog/webhook-security/) — verification, secrets and more. - [Try the free Webhook Bin](https://webhookrelay.com/blog/what-is-webhook/webhook-bin/) to see a real webhook in your browser. --- --- title: Receive GitHub Webhooks Locally | WebhookRelay meta: "og: title": "Receive GitHub Webhooks Locally" description: Receive GitHub webhooks locally and test them on localhost without deploying. Inspect the real payload, forward to your handler, and verify the signature. url: https://webhookrelay.com/blog/receive-github-webhooks-locally.md file: /blog/receive-github-webhooks-locally.md --- ![Stripes](https://webhookrelay.com/blog/receive-github-webhooks-locally/images/stripes.svg) # **Receive GitHub Webhooks Locally** Receive GitHub webhooks locally and test them on localhost without deploying. Inspect the real payload, forward to your handler, and verify the signature. ![Receive GitHub Webhooks Locally (Test GitHub Webhooks on localhost)](https://webhookrelay.com/blog/receive-github-webhooks-locally/images/blog/heroes/localhost.jpg) You are building a GitHub integration — a bot, a CI trigger, a deploy hook — and you need to see your handler react to a real `push` or `pull_request` event. The problem is immediate: GitHub will only POST to a **public URL**, and your handler is running on `localhost:8080`. GitHub has no way to reach it. The usual workarounds are painful. Deploying to a staging server for every code change is slow. Pasting payloads from the docs into curl gives you a _guess_ at the real request, not the real headers and body GitHub actually sends. What you want is to **receive GitHub webhooks locally** — real events, hitting your local handler, with a URL that does not change every time you restart. This guide shows how to do exactly that. ## [Why receiving GitHub webhooks locally is tricky](#why-receiving-github-webhooks-locally-is-tricky) A webhook is just an HTTP request that GitHub sends to a URL when something happens in your repo. GitHub sits on the public internet; your dev machine usually does not. It is behind a router, a corporate firewall, or both, with no public IP and no inbound ports open. So you need something in the middle: a public endpoint GitHub can hit, that relays each request down to your laptop without you opening a single firewall port. That is what [Webhook Relay](https://my.webhookrelay.com/register) does — and unlike a random tunnel URL, the endpoint is **stable**, so you configure GitHub once and never touch it again. ## [Step 1: Inspect the real payload with Webhook Bin](#step-1-inspect-the-real-payload-with-webhook-bin) Before you write any handler code, find out what GitHub actually sends. Open the free [Webhook Bin](https://webhookrelay.com/blog/receive-github-webhooks-locally/webhook-bin/) — no signup — and you get an instant public URL. 1. Copy the Webhook Bin URL. 2. In your repository, go to **Settings → Webhooks → Add webhook**. 3. Paste the URL into **Payload URL**, set **Content type** to `application/json`, and choose the events you care about (or "Send me everything"). 4. Click **Add webhook**. GitHub immediately fires a `ping` event to confirm the endpoint works — you will see it land in Webhook Bin right away. Trigger a real action (push a commit, open a pull request) and inspect the captured request: the full JSON body, the query string, and every header, including `X-GitHub-Event`, `X-GitHub-Delivery`, and `X-Hub-Signature-256`. Now you know the exact shape of the data before writing a line of code. For more on this approach, see [How to test webhooks](https://webhookrelay.com/blog/receive-github-webhooks-locally/blog/how-to-test-webhooks/) and [What is a webhook](https://webhookrelay.com/blog/receive-github-webhooks-locally/blog/what-is-webhook/). ## [Step 2: Forward the events to localhost with the relay agent](#step-2-forward-the-events-to-localhost-with-the-relay-agent) Once you know the payload, route those same events into your local handler. [Sign up for Webhook Relay](https://my.webhookrelay.com/register), install the `relay` agent (CLI or Docker), and create a bucket — say `github`. The bucket gives you a stable public input endpoint. Start forwarding to your local server: ``` relay forward --bucket github http://localhost:8080/webhook ``` The agent opens an **outbound** connection to Webhook Relay and streams every incoming request down to `http://localhost:8080/webhook`. Because the connection is outbound, there are **no firewall ports to open and no public IP needed** — this works from your laptop, behind a corporate proxy, or inside a Kubernetes cluster. Running in Docker? The same command works in the official `webhookrelay/webhookrelayd` image. Full details are in the [localhost forwarding docs](https://webhookrelay.com/blog/receive-github-webhooks-locally/docs/webhooks/internal/localhost/). Now update the GitHub webhook's **Payload URL** to your Webhook Relay endpoint (or just create it there from the start). Push a commit and watch it arrive on `localhost`. ## [GitHub-specific configuration and quirks](#github-specific-configuration-and-quirks) A few GitHub details worth knowing: - **Where to add it:** repository **Settings → Webhooks** for a single repo, or **organization Settings → Webhooks** to cover every repo in the org. - **Content type:** use `application/json` so the body is raw JSON. (The alternative, `application/x-www-form-urlencoded`, wraps the JSON inside a `payload` form field — easy to trip over.) - **Events:** pick individual events (push, pull_request, issues, release, …) or choose "Send me everything." Use the `X-GitHub-Event` header to branch in your handler. - **The ping event:** GitHub sends a `ping` the moment you create the webhook. It is a great sanity check that the endpoint is reachable. - **Redeliver:** under **Settings → Webhooks → Recent Deliveries**, every past delivery can be **re-sent** with the original payload and headers. This is the single most useful feature for local development — keep your `relay forward` running and replay a real delivery as many times as you need. ## [Step 3: Verify the GitHub webhook signature](#step-3-verify-the-github-webhook-signature) If you set a **Secret** on the webhook, GitHub signs each request. It computes an HMAC-SHA256 of the raw request body using your secret and sends the digest in the **`X-Hub-Signature-256`** header (formatted as `sha256=...`). Your handler should recompute the HMAC over the **raw** body and compare in constant time before trusting the payload. To sanity-check your implementation, paste a captured body, your secret, and the received signature into the free [GitHub signature verifier](https://webhookrelay.com/blog/receive-github-webhooks-locally/verify-github-webhook-signature/). For language-specific code and the common pitfalls (reading the body after a JSON parser has consumed it, timing-safe comparison), read [Verify a webhook signature](https://webhookrelay.com/blog/receive-github-webhooks-locally/blog/verify-webhook-signature/). ## [Replay and iterate](#replay-and-iterate) This is where local development gets fast: - **Replay from GitHub** via the **Redeliver** button to re-run a real delivery against your handler. - **Replay from Webhook Relay** — past requests are stored on your bucket, so you can resend a captured event without touching GitHub at all. - **Iterate on your handler** by editing code and replaying the same delivery until it behaves correctly. No commits, no pushes, no deploys just to test a code path. Because the Webhook Relay endpoint is stable, you can stop and restart the agent, reboot your machine, or come back next week — the GitHub configuration never needs to change. ## [Get started](#get-started) 1. Inspect the real payload in the free [Webhook Bin](https://webhookrelay.com/blog/receive-github-webhooks-locally/webhook-bin/) — no signup needed. 2. [Create a Webhook Relay account](https://my.webhookrelay.com/register), install the agent, and run `relay forward --bucket github http://localhost:8080/webhook`. 3. Point your GitHub webhook at the stable endpoint, trigger an event, and watch it hit `localhost`. You will be testing real GitHub events against your local handler in a few minutes — no deploys, no open firewall ports, and a URL you configure exactly once. --- --- title: How to Verify a Webhook Signature (HMAC SHA256) meta: "og: title": "How to Verify a Webhook Signature (HMAC SHA256)" description: Verify webhook signatures so you only trust authentic requests. How HMAC SHA256 works, how GitHub and Stripe do it, plus a Node.js example. url: https://webhookrelay.com/blog/verify-webhook-signature.md file: /blog/verify-webhook-signature.md --- ![Stripes](https://webhookrelay.com/blog/verify-webhook-signature/images/stripes.svg) # **How to Verify a Webhook Signature (HMAC SHA256)** Verify webhook signatures so you only trust authentic requests. How HMAC SHA256 works, how GitHub and Stripe do it, plus a Node.js example. Your webhook endpoint is a public URL. That means anyone who discovers it can send it requests — including fake ones. **Signature verification** is how you make sure a webhook genuinely came from the provider and wasn't tampered with in transit. Skip it, and an attacker could forge a `payment.succeeded` or `subscription.created` event. This guide explains how HMAC signing works, how the major providers implement it, and how to verify it correctly. ## [How webhook signing works](#how-webhook-signing-works) When a provider sends a webhook, it computes an **HMAC** — a hash of the request body mixed with a secret only you and the provider know — and puts the result in a header. You recompute the same HMAC on your side and compare. If they match, the body is authentic and unmodified, because an attacker can't produce a valid hash without the secret. The steps are always the same: 1. Read the **raw** request body (the exact bytes, not re-serialized JSON). 2. Compute `HMAC-SHA256(secret, rawBody)`. 3. Compare it to the signature header using a **constant-time** comparison. ## [How the major providers do it](#how-the-major-providers-do-it) | Provider | Header | Algorithm | Notes | | --- | --- | --- | --- | | GitHub | `X-Hub-Signature-256` | HMAC-SHA256, hex, prefixed `sha256=` | Strip the `sha256=` prefix before comparing | | Stripe | `Stripe-Signature` | HMAC-SHA256 over `timestamp.body` | Header contains `t=` and `v1=`; check the timestamp to prevent replays | | Shopify | `X-Shopify-Hmac-Sha256` | HMAC-SHA256, **base64** | Encode your digest as base64, not hex | | Slack | `X-Slack-Signature` | HMAC-SHA256 over `v0:timestamp:body` | Versioned signing string | | Square | `x-square-hmacsha256-signature` | HMAC-SHA256, base64 | Signs the **notification URL + raw body** — the URL must match exactly | | LINE | `x-line-signature` | HMAC-SHA256, base64 | Channel secret over the raw body; never deserialize first | | Airwallex | `x-signature` | HMAC-SHA256, hex | Signs `x-timestamp` (milliseconds) **+** body; prepend the timestamp | | MyFatoorah | `MyFatoorah-Signature` | HMAC-SHA256, base64 | Signs a canonical `Key=Value` string per event, **not** the raw body | The last four are worth a closer look because each breaks the "just HMAC the raw body" assumption: Square prepends your endpoint URL, Airwallex prepends a millisecond timestamp, and MyFatoorah signs a hand-built canonical string instead of the body at all. Get those details wrong and verification silently fails. ## [A Node.js example](#a-nodejs-example) ``` const crypto = require('crypto') // rawBody must be the exact bytes you received (Buffer or string), not JSON.parse'd output. function verifyGitHub(rawBody, signatureHeader, secret) { const digest = 'sha256=' + crypto .createHmac('sha256', secret) .update(rawBody) .digest('hex') // constant-time comparison avoids timing attacks const a = Buffer.from(digest) const b = Buffer.from(signatureHeader || '') return a.length === b.length && crypto.timingSafeEqual(a, b) } ``` For Shopify, swap `.digest('hex')` for `.digest('base64')` and drop the `sha256=` prefix. For Stripe, build the signed payload as `${timestamp}.${rawBody}` and compare against the `v1` value. ## [Test your verification with a free tool](#test-your-verification-with-a-free-tool) Before wiring this into production, confirm your inputs line up. Paste the raw body, the secret and the expected signature into the free [HMAC generator & signature verifier](https://webhookrelay.com/blog/verify-webhook-signature/hmac-verification/) — it computes the HMAC for SHA256, SHA512, SHA1 or MD5 and tells you whether it matches the provider's value. It's the fastest way to rule out "is my secret right?" before debugging code. ## [Provider-specific verifiers](#provider-specific-verifiers) The generic verifier works for any plain-HMAC provider, but for the ones below we built dedicated tools that bake in that provider's exact scheme — the right header, encoding, timestamp/URL handling and secret format — plus copy-paste Node.js and Python: - [Verify Stripe webhook signatures](https://webhookrelay.com/blog/verify-webhook-signature/verify-stripe-webhook-signature/) — `Stripe-Signature`, timestamp + body - [Verify GitHub webhook signatures](https://webhookrelay.com/blog/verify-webhook-signature/verify-github-webhook-signature/) — `X-Hub-Signature-256`, hex - [Verify Shopify webhook signatures](https://webhookrelay.com/blog/verify-webhook-signature/verify-shopify-webhook-signature/) — `X-Shopify-Hmac-Sha256`, base64 - [Verify Slack webhook signatures](https://webhookrelay.com/blog/verify-webhook-signature/verify-slack-webhook-signature/) — `v0:timestamp:body` - [Verify Square webhook signatures](https://webhookrelay.com/blog/verify-webhook-signature/verify-square-webhook-signature/) — `x-square-hmacsha256-signature`, signs URL + body - [Verify LINE webhook signatures](https://webhookrelay.com/blog/verify-webhook-signature/verify-line-webhook-signature/) — `x-line-signature`, channel secret - [Verify Airwallex webhook signatures](https://webhookrelay.com/blog/verify-webhook-signature/verify-airwallex-webhook-signature/) — `x-signature` + `x-timestamp` - [Verify MyFatoorah webhook signatures](https://webhookrelay.com/blog/verify-webhook-signature/verify-myfatoorah-webhook-signature/) — canonical `Key=Value` string - [Verify Twilio webhook signatures](https://webhookrelay.com/blog/verify-webhook-signature/verify-twilio-webhook-signature/) — URL + sorted params, HMAC-SHA1 - [Verify Standard Webhooks signatures](https://webhookrelay.com/blog/verify-webhook-signature/verify-standard-webhooks-signature/) — Svix/OpenAI/Anthropic - [Verify Zendesk webhook signatures](https://webhookrelay.com/blog/verify-webhook-signature/verify-zendesk-webhook-signature/) — `X-Zendesk-Webhook-Signature`, timestamp + body, base64 - [Verify DocuSign webhook signatures](https://webhookrelay.com/blog/verify-webhook-signature/verify-docusign-webhook-signature/) — `X-DocuSign-Signature-1`, base64, rotating keys - [Verify Klarna webhook signatures](https://webhookrelay.com/blog/verify-webhook-signature/verify-klarna-webhook-signature/) — `Klarna-Signature`, HMAC-SHA256 hex - [Verify Attio webhook signatures](https://webhookrelay.com/blog/verify-webhook-signature/verify-attio-webhook-signature/) — `Attio-Signature`, HMAC-SHA256 hex - [All providers →](https://webhookrelay.com/blog/verify-webhook-signature/verify-webhook-signature/) ## [Common mistakes](#common-mistakes) - **Verifying the parsed body.** Frameworks often parse JSON before you see it; re-serializing changes the bytes and breaks the signature. Capture the **raw** body. - **Using `==` to compare.** Use a constant-time comparison (`crypto.timingSafeEqual`) to avoid timing attacks. - **Wrong encoding.** GitHub is hex, Shopify is base64 — mixing them up fails silently. - **Ignoring the timestamp.** For Stripe/Slack, reject signatures with an old timestamp to prevent replay attacks. - **Forgetting the secret rotates.** Support more than one valid secret during rotation. ## [Where this fits](#where-this-fits) Signature verification is one item on a longer list — see [webhook security best practices](https://webhookrelay.com/blog/verify-webhook-signature/blog/webhook-security/) for the full checklist, and [how to test webhooks](https://webhookrelay.com/blog/verify-webhook-signature/blog/how-to-test-webhooks/) to capture real signed payloads to test against. Need to verify a signature right now? [Open the HMAC verifier](https://webhookrelay.com/blog/verify-webhook-signature/hmac-verification/), or [inspect a live webhook](https://webhookrelay.com/blog/verify-webhook-signature/webhook-bin/) to grab a real signature header. --- --- title: Webhook vs API: What's the Difference? (With Examples) meta: "og: title": "Webhook vs API: What's the Difference? (With Examples)" description: A webhook pushes, an API pulls. The real differences, when to use each, code examples for both, and the reliability trade-offs nobody warns you about. url: https://webhookrelay.com/blog/webhooks-vs-api.md file: /blog/webhooks-vs-api.md --- ![Stripes](https://webhookrelay.com/blog/webhooks-vs-api/images/stripes.svg) # **Webhooks vs API: What's the Difference?** A webhook pushes, an API pulls. The real differences, when to use each, code examples for both, and the reliability trade-offs nobody warns you about. If you're integrating two systems, you'll hit this question fast: should you call an **API**, or receive a **webhook**? They both move data over HTTP, but they work in opposite directions — and picking the right one saves you a lot of wasted requests. ## [The one-line answer](#the-one-line-answer) - **API:** _you pull._ Your app asks the other system for data when it needs it. - **Webhook:** _they push._ The other system sends you data automatically when something happens. ## [What is a webhook vs an API?](#what-is-a-webhook-vs-an-api) An **API** (specifically a web API) is an interface you call. You make the request, you choose the timing, and you get a response containing the current state of something. A **webhook** is a request you receive. You register a URL with a provider once, and from then on the provider makes the request — to you — every time a relevant event occurs. Both are HTTP. The difference isn't the technology, it's **who initiates the conversation**. That single inversion changes almost everything about how you build the integration: where the code runs, how you handle failure, and what you have to secure. ## [How an API works (pull)](#how-an-api-works-pull) With a typical web API, your application is the one making requests. You want to know if an order shipped, so you call the provider's endpoint and get the current state back: ``` GET /v1/orders/ord_123 # -> { "status": "shipped" } ``` If you want to stay up to date, you have to keep asking — **polling** every minute or so. Most of those requests return "nothing changed," which wastes time and rate limits. The arithmetic gets ugly quickly. Polling one endpoint every minute is **1,440 requests per day**, or about 43,000 a month. If ten orders actually change state in that time, you made 43,000 requests to learn ten things — a hit rate of roughly 0.02%. Multiply by every customer or resource you're tracking and polling becomes the dominant cost of the integration. Polling also has a latency floor. If you poll every 5 minutes, you learn about events with an **average delay of 2.5 minutes** and a worst case of 5. The only way to reduce that is to poll harder, which costs more. ## [How a webhook works (push)](#how-a-webhook-works-push) A webhook flips the direction. You give the provider a URL, and **they** send you an HTTP POST the moment an event occurs: ``` POST /your-webhook-endpoint Content-Type: application/json X-Stripe-Signature: t=1719849600,v1=5257a869e7... { "event": "order.shipped", "id": "ord_123" } ``` No polling, no wasted calls — you find out as it happens, typically within a second. That's why webhooks are sometimes called "reverse APIs" or "push APIs." (New to them? See [what is a webhook](https://webhookrelay.com/blog/webhooks-vs-api/blog/what-is-webhook/).) Ten events now cost you exactly ten requests instead of 43,000. ## [Webhooks vs API at a glance](#webhooks-vs-api-at-a-glance) | | API (pull) | Webhook (push) | | --- | --- | --- | | Who initiates | Your app | The provider | | Timing | On demand / polled | Real time, event-driven | | Efficiency | Wasteful if polling | Only fires on events | | Typical latency | Half your poll interval | ~1 second | | Needs a public URL | No | Yes (a URL providers can reach) | | Best for | Fetching current state | Reacting to events | | Failure handling | Retry your own call | Provider retries delivery | | Who's on the hook for uptime | The provider | **You** | | Auth direction | You send a key | You verify a signature | That second-to-last row is the one people underestimate. When you poll, an outage on your side just means you catch up on the next run. When you receive webhooks, an outage on your side means events are arriving at a door nobody is answering. ## [Is a webhook just a POST API?](#is-a-webhook-just-a-post-api) Mechanically, yes. There's no special protocol — a webhook is an ordinary HTTP POST with a JSON body. If you've written any endpoint that accepts POST, you already know how to receive one. What changes is **ownership**. With an API you control when the call happens, so you can retry, back off, and batch. With a webhook the provider controls the timing, so your engineering problem shifts to a different set of questions: - Is my endpoint reachable right now? - Is this request genuinely from the provider, or did someone find my URL? - Have I already processed this exact event? - What happens to events that arrive while I'm deploying? Those four questions are the entire difficulty of webhooks. The HTTP part is trivial. ## [API vs webhook vs WebSocket vs polling](#api-vs-webhook-vs-websocket-vs-polling) Push and pull isn't a binary — there are four common options, and they trade off differently: | | Direction | Connection | Latency | Best for | | --- | --- | --- | --- | --- | | **REST API** | You → provider | Per request | Immediate on request | On-demand reads and writes | | **Polling** | You → provider, on a timer | Per request | Half the interval | Providers with no webhooks | | **Webhook** | Provider → you | Per event | ~1 second | Discrete events, server-to-server | | **WebSocket** | Bidirectional | Persistent | Milliseconds | High-frequency streams, live UIs | The rough rule: **webhooks for events, WebSockets for streams.** A payment succeeding is an event — it happens occasionally and each one matters individually. A price ticker is a stream — it updates constantly and you mostly care about the latest value. See [webhook vs WebSocket](https://webhookrelay.com/blog/webhooks-vs-api/blog/webhook-vs-websocket/) for the full comparison. ## [What is an example of a webhook?](#what-is-an-example-of-a-webhook) The clearest examples come from services you already use: - **Payments** — Stripe POSTs `checkout.session.completed` when a customer pays, so you can fulfil the order without polling for payment status. See [receiving Stripe webhooks on localhost](https://webhookrelay.com/blog/webhooks-vs-api/blog/receiving-stripe-webhooks-localhost/). - **Code and CI** — GitHub POSTs a `push` event so your build system starts a pipeline the moment code lands. See [the GitHub + Jenkins guide](https://webhookrelay.com/blog/webhooks-vs-api/blog/github-jenkins-guide/). - **Ecommerce** — Shopify POSTs `orders/create` so your fulfilment system sees new orders immediately. - **Forms and messaging** — Typeform posts new submissions; Slack posts events from your workspace. If you want to see what real webhook traffic looks like in aggregate — which providers send the most, what payload sizes and content types dominate — we publish live figures from our own ingesters on [the State of Webhooks](https://webhookrelay.com/blog/webhooks-vs-api/state-of-webhooks/). ## [When should you use a webhook?](#when-should-you-use-a-webhook) Reach for a webhook when: - You need to **react to events in near real time** — payments, deploys, new messages, alerts. - You'd otherwise be **polling constantly** and mostly getting "nothing changed." - The provider is the **source of truth** for something you can't predict the timing of. - You're integrating **server to server** and can keep an endpoint available. ## [When should you use an API instead?](#when-should-you-use-an-api-instead) Reach for an API (or polling) when: - The provider **doesn't offer webhooks** for the event you care about. - You need a **full, current snapshot** on demand — reconciliation, dashboards, reports. - You only need the data **occasionally**, so an event stream is overkill. - You **can't expose an endpoint** at all, and a tunnel or gateway isn't an option. - You need **guaranteed completeness** and would rather sweep than trust delivery. ## [What are the disadvantages of webhooks?](#what-are-the-disadvantages-of-webhooks) This is where most guides stop being useful, so here's the honest list. **You need a publicly reachable URL.** Providers can only POST to a public address. Your handler probably runs on `localhost` or inside a private network, which is why webhook development is annoying in a way API development isn't. **Delivery is at-least-once, not exactly-once.** The same event can arrive twice — a retry after a timeout your server actually processed, for example. Handlers must be **idempotent**: deduplicate on the provider's event ID and make reprocessing a no-op. **Ordering is not guaranteed.** `order.updated` can land before `order.created`. If sequence matters, sort by a timestamp in the payload or reconcile against the API rather than trusting arrival order. **Retry policies vary wildly, and then stop.** Stripe retries with exponential backoff for up to about 3 days. Shopify retries 19 times over 48 hours. GitHub doesn't automatically retry at all — a failed delivery just sits in the log until you manually redeliver it. Check your specific provider's docs, because "the provider retries" is doing a lot of unexamined work in most comparisons. Once the retry window closes, the event is gone. **Anyone can POST to your URL.** A public endpoint is a public endpoint. Without signature verification, an attacker who discovers it can forge events — "payment succeeded" being the obvious nightmare. **Silent failure is the default.** If a provider disables your endpoint after repeated failures, nothing in your application notices. You have to monitor deliveries deliberately. None of these are reasons to avoid webhooks. They're the reason webhook infrastructure exists. ## [They're better together](#theyre-better-together) The most robust integrations use **both**. A common, reliable pattern is the _thin event_: the webhook tells you _that_ something happened, and you call the API to fetch the authoritative details. ``` order.shipped webhook arrives -> GET /orders/ord_123 to fetch full, current data ``` This avoids trusting a payload that might be stale or out of order, and keeps your webhook handler simple. It also neatly sidesteps the ordering problem: it doesn't matter which order the events arrive in if you always re-read current state. A good default architecture: 1. **Receive** the webhook and verify its signature. 2. **Acknowledge** with a `2xx` immediately — before doing any real work. 3. **Enqueue** the event ID for background processing. 4. **Fetch** the authoritative record from the API in the worker. 5. **Deduplicate** on the event ID so replays are safe. ## [Making webhooks reliable](#making-webhooks-reliable) Three rules cover most production incidents: **Respond `2xx` fast.** Providers time out aggressively — often in 5 to 10 seconds. Do your real work asynchronously. Returning `200` then processing in a queue is correct; processing inline and returning `200` forty seconds later is how you end up with duplicate deliveries. **Be idempotent.** Store processed event IDs and skip repeats. This is the single highest-value thing you can do, because it makes retries — yours and theirs — safe. **Don't lose events during deploys.** The gap where your endpoint is restarting is exactly when a provider will POST. Either buffer in front of your app with a [webhook gateway](https://webhookrelay.com/blog/webhooks-vs-api/webhook-gateway/), or use [durable retries](https://webhookrelay.com/blog/webhooks-vs-api/features/durable-retries/) so failed deliveries keep trying rather than evaporating. See [retries and idempotency](https://webhookrelay.com/blog/webhooks-vs-api/blog/webhook-retries-and-idempotency/) for the deeper treatment. ## [Securing a webhook endpoint](#securing-a-webhook-endpoint) With an API, you authenticate _outbound_ — you send a key. With a webhook it's reversed: you must prove the _inbound_ request is genuine. Almost every provider signs its webhooks with an HMAC over the raw request body, sent in a header like `Stripe-Signature` or `X-Hub-Signature-256`. Verification means recomputing that HMAC with your shared secret and comparing in constant time. Two things trip people up: - **Verify against the raw body**, not the re-serialized JSON. `JSON.parse` then `JSON.stringify` changes the bytes and every signature will fail. - **Check the timestamp** where the provider includes one, so a captured request can't be replayed later. You can check a signature against your secret in the browser with our [webhook signature verifier](https://webhookrelay.com/blog/webhooks-vs-api/verify-webhook-signature/) — including provider-specific pages for [Stripe](https://webhookrelay.com/blog/webhooks-vs-api/verify-stripe-webhook-signature/), [GitHub](https://webhookrelay.com/blog/webhooks-vs-api/verify-github-webhook-signature/), [Shopify](https://webhookrelay.com/blog/webhooks-vs-api/verify-shopify-webhook-signature/) and [Slack](https://webhookrelay.com/blog/webhooks-vs-api/verify-slack-webhook-signature/). The full write-up is in [how to verify a webhook signature](https://webhookrelay.com/blog/webhooks-vs-api/blog/verify-webhook-signature/) and [webhook security best practices](https://webhookrelay.com/blog/webhooks-vs-api/blog/webhook-security/). ## [The catch with webhooks: you need a reachable URL](#the-catch-with-webhooks-you-need-a-reachable-url) The one thing a webhook needs that an API call doesn't is a **public URL the provider can POST to** — which is awkward when your code runs on `localhost` or behind a firewall. That's exactly what Webhook Relay solves: [inspect the payload](https://webhookrelay.com/blog/webhooks-vs-api/webhook-bin/) in your browser, then [forward it to localhost or a private server](https://webhookrelay.com/blog/webhooks-vs-api/webhooks/) with no public IP. ``` relay forward --bucket my-app http://localhost:3000/webhooks ``` ## [Quick decision checklist](#quick-decision-checklist) Answer these in order: 1. **Does the provider offer a webhook for this event?** If no, poll — you're done. 2. **Do you need to know within seconds?** If no, polling is simpler and has fewer failure modes. 3. **Can you keep an endpoint available and verify signatures?** If not, use a gateway or a managed relay in front of your app. 4. **Does the payload contain everything you need?** If not — or if ordering matters — use the thin event pattern and call the API on receipt. Most production integrations end up at: **webhook to learn, API to confirm.** ## [Related reading](#related-reading) - [What is a webhook](https://webhookrelay.com/blog/webhooks-vs-api/blog/what-is-webhook/) — the full primer, with a Node.js example. - [Webhook vs WebSocket](https://webhookrelay.com/blog/webhooks-vs-api/blog/webhook-vs-websocket/) — push callbacks vs a persistent connection. - [Webhook architecture diagram](https://webhookrelay.com/blog/webhooks-vs-api/blog/webhook-architecture-diagram/) — how the pieces fit end to end. - [How to test webhooks](https://webhookrelay.com/blog/webhooks-vs-api/blog/how-to-test-webhooks/) — inspect, forward to localhost, and replay. - [The State of Webhooks](https://webhookrelay.com/blog/webhooks-vs-api/state-of-webhooks/) — live data on real webhook traffic. [Test a webhook now](https://webhookrelay.com/blog/webhooks-vs-api/webhook-bin/) or [start forwarding for free](https://my.webhookrelay.com/register). --- --- title: How to Test Webhooks: The Complete Guide (2026) meta: "og: title": "How to Test Webhooks: The Complete Guide (2026)" description: A practical guide to testing webhooks: get an instant URL, inspect payloads, receive webhooks on localhost, replay requests and verify signatures. url: https://webhookrelay.com/blog/how-to-test-webhooks.md file: /blog/how-to-test-webhooks.md --- ![Stripes](https://webhookrelay.com/blog/how-to-test-webhooks/images/stripes.svg) # **How to Test Webhooks: The Complete Guide (2026)** A practical guide to testing webhooks: get an instant URL, inspect payloads, receive webhooks on localhost, replay requests and verify signatures. Webhooks are awkward to test for one simple reason: **you don't control the sender, and your code usually runs on a machine the sender can't reach.** A provider POSTs to a public URL on its schedule, with a payload shape you have to discover, and your handler is sitting on `localhost`. This guide covers the practical ways to test webhooks — from "what is this payload?" to "does my handler work against real events?" — with tools you can use in the next five minutes. ## [What you actually need to test webhooks](#what-you-actually-need-to-test-webhooks) 1. **A public URL** the provider can call. 2. **Visibility** into the exact request (headers, body, query params). 3. **A way to get that request to your code** — usually on localhost. A webhook tester gives you the first two instantly. A tunnel or forwarding agent gives you the third. ## [Method 1: Inspect the payload with a webhook tester](#method-1-inspect-the-payload-with-a-webhook-tester) Before you write a line of handler code, see what the provider actually sends. Open [Webhook Bin](https://webhookrelay.com/blog/how-to-test-webhooks/webhook-bin/), a free [webhook.site alternative](https://webhookrelay.com/blog/how-to-test-webhooks/blog/webhook-site-alternative/) — you get a unique URL immediately, no signup: ``` curl -X POST https://your-bin-url \ -H 'Content-Type: application/json' \ -d '{"event":"order.created","id":"ord_123"}' ``` The request shows up in real time with the full method, headers, query string and body. Point Stripe, GitHub, Shopify or any provider at the URL and trigger an event to capture a **real** payload you can build against. ## [Method 2: Receive webhooks on localhost](#method-2-receive-webhooks-on-localhost) Once you know the shape, you want the webhook to hit your actual handler. Providers can't reach `localhost`, so run the [relay agent](https://webhookrelay.com/blog/how-to-test-webhooks/webhooks/) and forward incoming webhooks to your local port: ``` # one-time relay forward --bucket my-app http://localhost:8080/webhook ``` Now every webhook sent to your Webhook Relay endpoint is delivered to `localhost:8080/webhook`. The agent connects outbound, so there are **no firewall ports to open**, and the public URL is **stable** — set it once in the provider and keep developing. See the [localhost forwarding docs](https://webhookrelay.com/blog/how-to-test-webhooks/docs/webhooks/internal/localhost/) for details. > Prefer a raw tunnel? Webhook Relay also offers general-purpose [localhost tunnels](https://webhookrelay.com/blog/how-to-test-webhooks/tunnels/) if you just need to expose a port. ## [Method 3: Replay requests and fake responses](#method-3-replay-requests-and-fake-responses) Two things make iterating fast: - **Replay.** Capture a request once, then resend it to your endpoint as many times as you like. You re-run your handler without re-triggering the source event (no need to create another Stripe payment to test the `payment.succeeded` path). - **Custom responses.** Configure the status code, body and delay your endpoint returns, so you can test how a provider reacts to a `500`, a slow response, or a specific body. ## [Verify the signature while you're at it](#verify-the-signature-while-youre-at-it) Most providers sign webhooks with an HMAC so you can confirm authenticity. Test your verification logic with the free [HMAC generator & signature verifier](https://webhookrelay.com/blog/how-to-test-webhooks/hmac-verification/): paste the raw body, the secret and the signature header the provider sent, and confirm they match. GitHub uses `X-Hub-Signature-256` (SHA256), Stripe and Shopify also use SHA256. See [webhook security best practices](https://webhookrelay.com/blog/how-to-test-webhooks/blog/webhook-security/) for the full checklist. ## [Testing specific providers](#testing-specific-providers) The flow is the same for every provider — inspect, forward, replay — but each has its own quirks: - **Stripe:** use the dashboard's "Send test webhook" or the Stripe CLI, and point the endpoint at your Webhook Relay URL. Full walkthrough: [receive Stripe webhooks on localhost](https://webhookrelay.com/blog/how-to-test-webhooks/blog/receiving-stripe-webhooks-localhost/). - **GitHub:** add the URL under Settings → Webhooks and use "Redeliver" to replay. See the [GitHub + Jenkins guide](https://webhookrelay.com/blog/how-to-test-webhooks/blog/github-jenkins-guide/). - **Shopify / PayPal / Twilio:** add the URL in the provider's webhook settings and trigger a test event. ## [Automated and CI testing](#automated-and-ci-testing) For repeatable tests, save a few captured payloads as fixtures and POST them at your handler in CI. Because Webhook Relay endpoints are stable and support [transformations](https://webhookrelay.com/blog/how-to-test-webhooks/features/transform-webhooks/) and [filtering](https://webhookrelay.com/blog/how-to-test-webhooks/features/forwarding-rules/), you can also route only the events you care about to a test environment. ## [Common pitfalls](#common-pitfalls) - **Responding too slowly or with the wrong status.** Always return `2xx` quickly; do heavy work asynchronously, or providers will retry. - **Verifying against the parsed body.** Sign and verify against the **raw** request body — re-serializing JSON changes bytes and breaks the signature. - **Assuming ordering.** Webhooks can arrive out of order; design handlers to be idempotent. - **Hard-coding a tunnel URL that changes.** Use a stable endpoint so you configure the provider once. ## [Start testing](#start-testing) [Open Webhook Bin](https://webhookrelay.com/blog/how-to-test-webhooks/webhook-bin/) to inspect a request now, or [create a free account](https://my.webhookrelay.com/register) to forward webhooks to localhost and replay them against your code. --- --- title: ngrok Alternative: Persistent Webhook URLs | WebhookRelay meta: "og: title": "ngrok Alternative: Persistent Webhook URLs" description: An ngrok alternative for webhooks: a persistent URL that never changes, shared across your team so everyone receives the same Stripe or GitHub events. url: https://webhookrelay.com/blog/ngrok-alternative.md file: /blog/ngrok-alternative.md --- ![Stripes](https://webhookrelay.com/blog/ngrok-alternative/images/stripes.svg) # **An ngrok Alternative for Webhooks: Persistent URLs & Team Broadcast (2026)** An ngrok alternative for webhooks: a persistent URL that never changes, shared across your team so everyone receives the same Stripe or GitHub events. ![An ngrok Alternative for Webhooks: Persistent URLs and Team Broadcast (2026)](https://webhookrelay.com/blog/ngrok-alternative/images/blog/heroes/alternative.jpg) If you searched for an **ngrok alternative**, you probably hit one of ngrok's free-plan limits: the 2-hour session timeout, the random URL that changes every time you restart, or the fact that a tunnel belongs to exactly one person. ngrok is a great general-purpose tunnel — but if your real job is _receiving webhooks_, especially across a team, a plain tunnel leaves the important parts to you. Webhook Relay comes at it from the webhook side: a **persistent URL that never changes**, and a **shared bucket your whole team can connect to** so everyone receives the same events at once. ## [TL;DR](#tldr) - **Tired of re-pasting a URL that changes on every restart?** Webhook Relay's endpoint URL is persistent on every plan, including free — set it once in Stripe, Twilio or GitHub and forget it. - **Whole team needs the same test webhooks?** Point one bucket at Stripe/Twilio and every developer connects their agent to it — everyone's local backend receives the same events simultaneously. ngrok can't share one endpoint like this. - **Webhook has to reach a private server, container, or CI runner?** The relay agent connects outbound and delivers into private networks with no public IP — a plain tunnel can't. - **ngrok is still the better pick** for ad-hoc TCP/SSH tunnels and quick one-off port exposure. ## [ngrok vs Webhook Relay at a glance](#ngrok-vs-webhook-relay-at-a-glance) | | ngrok (free) | Webhook Relay | | --- | --- | --- | | Session timeout | ~2 hours | None | | Persistent URL on restart | Paid reserved domains only | Yes, every plan | | Share one endpoint across a team (broadcast) | No — one tunnel per process | [Yes](https://webhookrelay.com/blog/ngrok-alternative/webhooks/) — every agent gets every event | | Inspect requests in the browser | Yes (web inspector) | Yes ([Webhook Bin](https://webhookrelay.com/blog/ngrok-alternative/webhook-bin/)) | | Forward to localhost / private server | Via the agent | Via the [relay agent](https://webhookrelay.com/blog/ngrok-alternative/webhooks/) | | Durable retries if the receiver is offline | No | [Yes, up to 30 days](https://webhookrelay.com/blog/ngrok-alternative/features/durable-retries/) | | Transform payloads (JS/Lua) | No | [Yes](https://webhookrelay.com/blog/ngrok-alternative/features/transform-webhooks/) | | Fan-out to multiple destinations | No | [Yes](https://webhookrelay.com/blog/ngrok-alternative/features/webhook-multiple-destinations/) | | Scheduled / cron webhooks | No | [Yes](https://webhookrelay.com/blog/ngrok-alternative/cron/) | | Kubernetes operator | No | Yes | | Free plan for real webhook work | Limited | Yes | | Starting paid price | ~$8–20/mo | $9.99/mo | _Competitor details reflect publicly documented plans as of 2026 and can change — check ngrok's current pricing and features before deciding._ ## [Where ngrok shines](#where-ngrok-shines) Let's be fair. ngrok is excellent at what it was built for: - **General-purpose tunnels**, including raw TCP and SSH. - A **polished traffic inspector** with replay. - A huge brand and ecosystem, recently expanded into an "AI & API gateway." If you need to occasionally expose an arbitrary local port for a quick demo, ngrok is hard to beat. ## [Where Webhook Relay wins for webhooks](#where-webhook-relay-wins-for-webhooks) ### [1. A persistent URL you configure once](#_1-a-persistent-url-you-configure-once) The most common ngrok complaint is the **changing URL**. On the free plan every restart hands you a new random subdomain, so you re-paste it into every provider's webhook settings. With Webhook Relay, your bucket's endpoint URL is **fixed for the life of the bucket** — set it once in Stripe, Twilio or GitHub and never touch it again. Want it on your own hostname? Add a [custom subdomain or domain](https://webhookrelay.com/blog/ngrok-alternative/features/custom-subdomains/). The URL surviving restarts, redeploys and laptop reboots is the whole point. ### [2. One bucket, your whole team](#_2-one-bucket-your-whole-team) This is the difference a tunnel can't close. In Webhook Relay, **multiple agents can connect to the same bucket, and every connected agent receives every webhook** — a broadcast, not a hand-off. Picture a team testing Stripe and Twilio integrations: 1. Create one bucket, say `acme-payments-test`, and give it a persistent URL. 2. Point Stripe's and Twilio's **test** webhooks at that URL — once, forever. 3. Every developer runs the agent against the same bucket: ``` relay forward --bucket acme-payments-test http://localhost:8080/webhooks ``` Now when a teammate triggers a test payment, **every developer's local backend receives that same webhook at the same time**. No one is hogging "the" tunnel, no one is re-pasting URLs into the Stripe dashboard, and a new hire is productive the moment they run one command. Everyone has a working local backend fed by real test events. ngrok hands each tunnel to a single process. There's no shared endpoint that fans one event out to the whole team — so in practice one person owns the webhook URL and everyone else is blocked or improvising. ### [3. Deliver to localhost _and_ private networks](#_3-deliver-to-localhost-and-private-networks) ngrok exposes a port. Webhook Relay **routes a webhook** to wherever it needs to go — your laptop on `localhost:8080`, an internal API behind a firewall, or a Kubernetes service with no public IP. The agent makes an **outbound** connection, so there are no firewall ports to open. See [webhooks to internal servers](https://webhookrelay.com/blog/ngrok-alternative/features/webhook-to-internal-server/). ### [4. Events wait when your receiver is down](#_4-events-wait-when-your-receiver-is-down) Close your laptop, redeploy, or restart mid-test and a plain tunnel simply drops whatever arrives. Webhook Relay persists every event and keeps retrying with [durable retries](https://webhookrelay.com/blog/ngrok-alternative/features/durable-retries/) — for up to 30 days — so the webhook that fired while you were rebooting still lands. ### [5. Do something to the webhook in flight](#_5-do-something-to-the-webhook-in-flight) Because Webhook Relay sits in the path, you can [transform payloads](https://webhookrelay.com/blog/ngrok-alternative/features/transform-webhooks/) with JavaScript or Lua (turn a raw GitHub event into a Slack message), [fan-out to multiple destinations](https://webhookrelay.com/blog/ngrok-alternative/features/webhook-multiple-destinations/), filter noisy events, verify signatures or retry on failure. A tunnel just moves bytes. In fact, forwarding is only one part of what Webhook Relay does — it's a full [webhook gateway](https://webhookrelay.com/blog/ngrok-alternative/webhook-gateway/) with authentication, throttling and delivery logs. ### [6. A real free tier for testing](#_6-a-real-free-tier-for-testing) Open [Webhook Bin](https://webhookrelay.com/blog/ngrok-alternative/webhook-bin/), get an instant URL, and watch requests arrive in real time — no signup, no install, no 2-hour clock. When you're ready to forward them somewhere, install the agent. ## [The same pattern powers your CI/CD](#the-same-pattern-powers-your-cicd) A CI/CD runner has exactly the ngrok problem, amplified: it's **ephemeral and usually private**, so a provider can't reach it at a stable public address. The persistent-URL-plus-outbound-agent model solves it — a self-hosted GitHub Actions runner, a Jenkins server behind a firewall, or a Harness delegate can all receive GitHub, GitLab or provider webhooks without a public IP, and the provider configuration never changes even as runners come and go. We wrote this up in depth: **[receiving webhooks in CI/CD — GitHub Actions, Jenkins and Harness](https://webhookrelay.com/blog/ngrok-alternative/blog/webhooks-in-ci-cd/)**. If Jenkins is your world, start with the [GitHub → Jenkins guide](https://webhookrelay.com/blog/ngrok-alternative/blog/github-jenkins-guide/) or [webhooks to Jenkins on Kubernetes](https://webhookrelay.com/blog/ngrok-alternative/blog/webhooks-to-jenkins-on-kubernetes/). ## [How to switch from ngrok in 2 minutes](#how-to-switch-from-ngrok-in-2-minutes) 1. **Inspect first (no install):** open [Webhook Bin](https://webhookrelay.com/blog/ngrok-alternative/webhook-bin/), copy the URL, and point your provider at it. 2. **Forward to localhost:** [create a free account](https://my.webhookrelay.com/register), install the agent, and run `relay forward`. 3. **Share with the team:** have each teammate run the same `relay forward --bucket ` command — everyone gets the same events. 4. **Keep the URL forever:** your endpoint doesn't change, so you never re-configure the provider. Full steps are in the [tunnels documentation](https://webhookrelay.com/blog/ngrok-alternative/docs/tunnels/demoing-your-website/). ## [When to pick which](#when-to-pick-which) - **Pick ngrok** for ad-hoc TCP/SSH tunnels and quick one-off port exposure. - **Pick Webhook Relay** when the work is webhooks: a persistent URL, a bucket your whole team can share, delivery into private infrastructure and CI runners, and durable retries instead of dropped events. Ready to stop re-pasting tunnel URLs? [Start forwarding for free](https://my.webhookrelay.com/register) or [test a webhook now](https://webhookrelay.com/blog/ngrok-alternative/webhook-bin/). ![Stripes](https://webhookrelay.com/blog/ngrok-alternative/images/stripes-dark.svg) ## **Start forwarding webhooks in minutes ** Connect a source, pick a destination, and Webhook Relay handles delivery, retries and transforms. Set up your first webhook in under five minutes. [Start for free ->](https://my.webhookrelay.com/register) Free plan · No credit card required · 7-day money-back guarantee on paid plans --- --- title: Terms of Service | WebhookRelay meta: "og: title": "Terms of Service" description: Last updated on: Nov 25th, 2019 url: https://webhookrelay.com/tos.md file: /tos.md --- ![Stripes](https://webhookrelay.com/tos/images/stripes.svg) # **Terms of Service** Last updated on: Nov 25th, 2019 Last updated on: Nov 25th, 2019 ## [Agreement](#agreement) These Terms of Service (the "Terms") cover your use of the services Webhook Relay provides, including the webhookrelay.com, my.webhookrelay.com websites (the "Site"), the Webhook Relay API, the Webhook Relay tunneling service, the Webhook Relay client software, and any other software or services offered by Webhook Relay in connection with any of the above services (the "Webhook Relay Services" or the "Services"). "Webhook Relay" means the owner and operator of the Webhook Relay Services which distributes the Webhook Relay software and services. You must agree to the Terms in order to use the Services. Your use of the Webhook Relay means the owner and operator of the Webhook Relay Services which distributes the Webhook Relay software and services. Services means your acceptance of and agreement to the Terms. Webhook Relay will treat your use of any portion of the Services as acceptance of, and agreement to, the Terms from that point on. Webhook Relay may make changes to the Terms from time to time. We will provide notice on the dashboard of the Webhook Relay service if the Terms change in any substantive way. We will provide at least seven (7) days' notice before the changes take effect, during which period of time you may reject the changes by terminating your account. We may terminate or suspend access to the Services immediately, without prior notice or liability (other than refunding pre-paid fees to the extent we terminate based on no action or omission on your part), for any reason whatsoever, including, but not limited to, if you breach any of the Terms. All provisions of the Terms which, by their nature, should survive termination shall survive termination,including, without limitation, ownership provisions, warranty disclaimers, indemnifications and limitations of liability. 1.1. Your use of the Webhook Relay service (the “Service”) is governed by this agreement (the “Terms”). “Webhook Relay” means AppScension Ltd product, located at 4 Rolfe Terrace, London, SE18 6BS and its subsidiaries or affiliates involved in providing the Service. ## [Use of the Service](#use-of-the-service) Your use of the Service must comply with all applicable laws, regulations and ordinances, including any laws regarding the export of data or software. You agree not to use the Service in the design, development, production, or use of missiles or the design, development, production, stockpiling, or use of chemical or biological weapons. The Webhook Relay Services are not directed to children under the age of 14. ## [Registration and Your Webhook Relay Account](#registration-and-your-webhook-relay-account) Some Webhook Relay Services are available only if you register and create an account with Webhook Relay. If you create a Webhook Relay account, you must provide full and accurate information for such registration and account; you are responsible for maintaining the security of your account, your passwords and authentication tokens; and you are fully responsible for all activities that occur under the account and any other actions taken in connection with the account. You must immediately notify Webhook Relay of any unauthorized uses of your account or any other breaches of security. Webhook Relay will not be liable for any acts or omissions by you, including any damages of any kind incurred as a result of such acts or omissions. The personal information you provide to Webhook Relay for the purpose of creating and maintaining your account is governed by the Webhook Relay privacy policy: [https://webhookrelay.com/privacy](https://webhookrelay.com/privacy). You agree not to (a) access (or attempt to access) the administrative interface of the Service by any means other than through the interface that is provided by Webhook Relay in connection with the Service, unless you have been specifically allowed to do so in a separate agreement with Webhook Relay, or (b) engage in any activity that interferes with or disrupts the Service (or the servers and networks which are connected to the Service). ## [Responsibility and Conduct](#responsibility-and-conduct) You are solely responsible for the actions of all users of your account and any data that is created, stored, displayed by, or transmitted through your account while using Webhook Relay. You will not engage in any activity that interferes with or disrupts the Services or networks connected to Webhook Relay. ## [Copyright and Limited License](#copyright-and-limited-license) Unless otherwise indicated in the Site, the Site and all content and other materials on the Site and accessible through the Service, including, without limitation, all designs, text, graphics, pictures, information, data, software, sound files, other files and the selection and arrangement thereof (collectively, the "Site Materials") are the proprietary property of Webhook Relay or its licensors and are protected by and international copyright laws. You are granted a limited, non-sublicensable license to access and use the Site and the Webhook Relay Site Materials solely in connection with the Services. Such license is subject to this Agreement and does not include: (a) any resale or commercial use of the Site or the Site Materials therein; (b) the distribution, public performance or public display of any Site Materials except in connection with your authorized use of the Site and the Services; (c) modifying or otherwise making any derivative uses of the Site and/or the Site Materials, or any portion thereof; (d) use of any data mining, robots, page scraping or similar data gathering or extraction methods; (e) reverse engineering or attempting to reverse-engineer any functionality provided on the Site or Site Materials or (f) any use of the Site or the Site Materials other than for its intended purpose. ## [Trademarks](#trademarks) Webhook Relay, and any other product or service name or slogan contained in the Site are trademarks of Webhook Relay and its suppliers or licensors (unless otherwise indicated), and may not be copied, imitated or used, in whole or in part, without the prior written permission of Webhook Relay or the applicable trademark holder. In addition, the look and feel of the Site, including all page headers, custom graphics, button icons and scripts, is the service mark, trademark and/or trade dress of Webhook Relay and may not be copied, imitated or used, in whole or in part, without our prior written permission. Reference to any products, services, processes or other information, by trade name, trademark, manufacturer, supplier or otherwise does not constitute or imply endorsement, sponsorship or recommendation thereof by us. ## [Copyright Policy](#copyright-policy) Webhook Relay respects the intellectual property rights of others, and requests that users of the Webhook Relay website and Service do the same. All content, including copyrightable works, trademarks, service marks, and patentable inventions, on the Webhook Relay website are the property of Webhook Relay or Webhook Relay licensors unless explicitly stated otherwise. No right, title, or interest to the content is granted by your use of the Site, other than a right to review the content using a conventional Internet browser (i.e., ordinary web browsing). Any other uses, including making copies of any content, are strictly prohibited. If you believe that your work has been copied on the Webhook Relay Site in a way that constitutes copyright infringement, please provide Webhook Relay's Copyright Agent with a “Notification of Alleged Infringement.” It is Webhook Relay's policy to respond to clear Notifications of Alleged Infringement. The address for Webhook Relay's registered Copyright Agent is: [info@webhookrelay.com](https://webhookrelay.com/tos//mailto:info@webhookrelay.com) ## [Subscription Plans and Fees](#subscription-plans-and-fees) Webhook Relay provides both subscription services, that need to be paid on a recurring basis, and free services up to 150 Webhooks only. A subscription plan may subject you to recurring fees on a monthly, annually, or other agreed upon basis, as set forth in the pricing and payment terms presented to you for such service, in advance of providing the service. By signing up for a subscription plan, including after any free trial period, you agree to pay us the subscription fee and any applicable taxes. At the discretion of Webhook Relay, certain clients may subscribe to Webhook Relay on ‘as used’ or transactional basis only plans. Any such tailored plan may subject you to fees charged per usage and/or terms, including transaction volume. By subscribing to such plan, you agree to pay the fees and any taxes incurred at the time of usage. In general, any paid subscription plans may be paid by debit card, credit card or via invoice. If you link a debit or credit card to your account, you authorise Webhook Relay, as a collection agent, to collect paid subscription fees by debit from your linked debit card or charge to your linked credit card. You acknowledge that Webhook Relay may prospectively change the specified rates and charges from time to time. Webhook Relay is not responsible for any additional bank fees, interest charges, finance charges, overdraft charges, or other fees resulting from charges billed by Webhook Relay. Currency exchange settlements will be based on agreements between you and the provider of your credit card. ## [Fair Usage Policy](#fair-usage-policy) Any client exceeding the upper limit of webhooks usage as defined in their respective plan and/or terms, for 2 consecutive months, may be auto enrolled into a plan that most accurately reflects their current usage, or may be temporarily suspended from using Webhook Relay services. During this time, Webhook Relay may send a warning email to the user notifying them on any changes that may be to their current subscription plan. The Services may be interrupted on accounts that reach 5 days past due. Fees paid by you are non-refundable, except as provided in these Terms or when required by law. Webhook Relay reserves the right to discontinue the provision of the Service to you for any late payments. You will receive a pro rata credit of any previously paid fees. Subject to the Terms, certain Webhook Relay Services are provided to you without charge, up to certain specified limits. Usage over these limits requires your purchase of additional resources or services. ## [Exclusion of Warranties](#exclusion-of-warranties) You expressly understand and agree that your use of the service is at your sole risk and that the services are provided “as is” and “as available. Nothing in these terms, shall exclude or limit Webhook Relay’s warranty or liability for losses which may not be lawfully excluded or limited by applicable law. Webhook Relay makes no express warranties and disclaims all express and implied warranties regarding the services, including express and implied warranties of merchantability, fitness for a particular purpose and non-infringement. Without limiting the generality of the foregoing, Webhook Relay does not represent or warrant to you that: (A) your use of the services will meet your requirements, (B) your use of the services will be uninterrupted, timely, secure or free from error. ## [Limitation of Liability](#limitation-of-liability) You expressly understand and agree that Webhook Relay shall not be liable to you under any legal theory for any direct, indirect, incidental, special consequential or exemplary damages which may be incurred by you, however caused and under any theory of liability. This shall include, but not limited to, any loss of profit (whether incurred directly or indirectly), any loss of goodwill or business reputation, any loss of data suffered, cost of procurement of substitute goods or services, or other intangible loss. To the extent that our liability may not be so limited, you acknowledge that Webhook Relay’s liability under any legal theory may not in any event exceed an amount equivalent to the charges actually paid by you for services during the six-month period preceding the event giving rise to such liability. In no event shall Webhook Relay be liable for any special or consequential damages. The limitations on Webhook Relay’s liability to you in shall apply whether or not Webhook Relay has been advised of or should have been aware of the possibility of any such losses arising. ## [General Representation and Warranty](#general-representation-and-warranty) You represent and warrant that your use of the Webhook Relay website and Services will be in strict accordance with the webhookrelay.com Privacy Policy, with this Agreement and with all applicable laws and regulations (including without limitation any local laws or regulations in your country, state, city, or other governmental area, regarding online conduct and acceptable content, and including all applicable laws regarding the transmission of technical data exported from the country in which you reside). ## [Indemnification](#indemnification) You agree to defend, hold harmless and indemnify Webhook Relay, and its subsidiaries, affiliates, officers, agents, employees, advertisers, licensors, suppliers or partners from and against any third-party claim arising from or in any way related to (A) your breech of the terms, (B) your use of the services, (C) your violation of applicable laws, rules or regulations in connection with the services, or (D) content, Made available over tunnels, including any liability or expense arising from any claims, losses, damages (actual and consequential), suits, judgements, litigation costs and attorney’s fees, of evert kind and nature. In such a case, Webhook Relay will provide you with written notice of such claim, suit or action. ## [Notice](#notice) You agree that Webhook Relay may provide you with notices, including those regarding changes to the Terms, by email, regular mail, or postings on the Webhook Relay website. ## [Entire Agreement](#entire-agreement) The Terms (including any policies, guidelines or amendments that may be presented to you from time to time) constitute the entire agreement between you and Webhook Relay and govern your use of the Webhook Relay Services, superseding any prior agreements between you and Webhook Relay for the use of the Webhook Relay Services. ## [Waiver and Severability of Terms](#waiver-and-severability-of-terms) The failure of Webhook Relay to exercise or enforce any right or provision of the Terms shall not constitute a waiver of such right or provision. If any provision of the Terms is found by a court of competent jurisdiction to be invalid, the parties nevertheless agree that the court should endeavor to give effect to the party's intentions as reflected in the provision, and the other provisions of the Terms remain in full force and effect. ## [Statute of Limitations](#statute-of-limitations) You agree that regardless of any statute or law to the contrary, any claim or cause of action arising out of or related to use of the Webhook Relay Services or the Terms must be filed within one (1) year after such claim or cause of action arises or be forever barred. --- --- title: Guide to the General Data Protection (GDPR) | WebhookRelay meta: "og: title": "Guide to the General Data Protection (GDPR)" description: Last updated on: June 1st, 2018 url: https://webhookrelay.com/gdpr.md file: /gdpr.md --- ![Stripes](https://webhookrelay.com/gdpr/images/stripes.svg) # **Guide to the General Data Protection (GDPR)** Last updated on: June 1st, 2018 ## [Data Processing Statement](#data-processing-statement) In the interest of transparency and [GDPR](https://gdprexplained.eu/) compliance, this page explains how the [Webhook Relay](https://webhookrelay.com) handles and processes your personal information on webhookrelay.com. This is a living document and will be updated with new information as our services evolve. The last updated was June 1st, 2018. You may also be interested in our [Terms of Service](https://webhookrelay.com/tos/) and [Privacy Policy](https://webhookrelay.com/privacy/). ## [The my.webhookrelay.com Website](#the-mywebhookrelaycom-website) Our website uses cookies when you log in to your account. Cookies store a unique token that identifies you. Our website generates standard access- and error logs, which are kept for operational reasons. These logs include IP addresses and information about your web browser and operating system version. They are kept for a maximum of 3 months although usually it's a lot shorter period, and then automatically deleted. ## [Your Account Details](#your-account-details) Your account details; your name, e-mail address and any custom text you have requested we include on your invoices are all visible on your [account page](https://my.webhookrelay.com/account). Note that we do not require our users provide us with real names or custom text, this option is a convenience which allows us to compose friendlier e-mails and make sure your receipts contain the information you need for your own accounting purposes. You are welcome to use a pseudonym or alias. You can also delete your account at any time by using the 'DELETE ACCOUNT' button on that page. The changes made on this page have immediate effect; however old information may remain present in secondary locations, such as: - The internal system audit log - E-mail archives - System backups ## [Data Transmitted Over Webhook Relay Tunnels](#data-transmitted-over-webhook-relay-tunnels) We do not routinely log or store any of the content transmitted over the Webhook Relay tunneling infrastructure. Exceptions to this will be made only temporarily, if necessary to troubleshoot and diagnose system failures, or to comply with legal requirements such as a subpoena. In practice, as of May 2018, this has never happened - we have never had cause to inspect our customers' traffic. However, we reserve the right to do so if it becomes necessary for operational or legal reasons. For operational reasons, our relays keep logs of the following meta-data for up to 7 days: - Number of requests made (also used for rate-limit enforcement) - Bytes transmitted for each tunnel - Tunnel names and protocols used for connections - Dates, times, bytes transmitted and connection speed for each request No backups are kept of relay logs and IP addresses are not collected. Tunnel servers have no access to Account Details and are designed to be standalone. ## [Data Transmitted When Using Webhooks Forwarding](#data-transmitted-when-using-webhooks-forwarding) Webhooks, forwarded through [buckets](https://my.webhookrelay.com/buckets) are recorded in the database (request body, headers and destination) for debugging, auditing or resend purposes. It is stored as long as account subscription plan allows it and then gets automatically deleted. Webhook Relay service only tracks the amount of webhooks forwarded in the current month for billing purposes. Account webhook content data cannot be accessed by anyone, except account owner or any sub-accounts that the account owner has created. ## [Third Parties: Payment Processors](#third-parties-payment-processors) We delegate payment processing to [Stripe](http://stripe.com). Their privacy policies may be found on their respective websites. When a payment is made, our payment processors forward to us the information required for us to validate and process your order. This includes most of the information you have provided them with during the payment process, except for credit card details which we never see. This information is in turn used to provide you with a receipt and update your account quotas. ## [Third Parties: Hosting Providers](#third-parties-hosting-providers) Webhook Relay rents world-wide computing and network capacity from of a variety of different providers, including: - [Google Cloud](https://cloud.google.com/) - main infrastructure provider - [Cloudflare](http://cloudflare.com/) - DNS provider and website hosting - [Stripe](http://stripe.com/) - payment processor Most of these servers are tunneling servers which have access to very little personal information and are configured to delete all local logs after only a few days have passed. The exception is our web server and account database servers, which also have the public roles of API and DNS servers. These servers are hosted with Google Cloud and our hosting provider also makes backups of live system data on our behalf. In all cases, the services provided by these Third Parties to Webhook Relay are content agnostic server capacity, public IP addresses and network bandwidth. We do not grant these providers any access to information about individual customers, although by nature of the services they provide, they do store such data on our behalf. We would consider it a breach of trust and a breach of contract if any of these providers were to access our server disks and extract private information about our customers. For any GDPR related queries, feel free to contact us on [info@webhookrelay.com](https://webhookrelay.com/gdpr//mailto:info@webhookrelay.com). --- --- title: Webhook Security: Best Practices to Secure Your Webhooks meta: "og: title": "Webhook Security: Best Practices to Secure Your Webhooks" description: Essential measures to fortify your webhooks — security best practices for reliable, safe data transfer that protect your applications from attack. url: https://webhookrelay.com/blog/webhook-security.md file: /blog/webhook-security.md --- ![Stripes](https://webhookrelay.com/blog/webhook-security/images/stripes.svg) # **Webhook Security: Best Practices to Secure Your Webhooks** Essential measures to fortify your webhooks — security best practices for reliable, safe data transfer that protect your applications from attack. ## [Introduction](#introduction) Webhooks have become an essential component of modern application integration, allowing systems to communicate and exchange information in real-time. However, ensuring the security of webhooks is crucial to protect against potential vulnerabilities and attacks. In this research report, we will explore the best practices for securing webhooks, including encryption, authentication, message verification, and more. By following these practices, you can enhance the security of your webhook implementation and protect your data from potential threats. ![firewall between webhooks and the server](https://webhookrelay.com/blog/webhook-security/images/blog/webhook-security/cover.png) ## [Best Practices for Webhook Security](#best-practices-for-webhook-security) #### [1. Encrypt Data Sent Through Webhooks](#_1-encrypt-data-sent-through-webhooks) Encrypting data sent through webhooks is a fundamental security measure to protect the confidentiality of the information transmitted. It is recommended to use the secure HTTP protocol, HTTPS, instead of HTTP. HTTPS encrypts all communication between the sender and receiver, making it harder for third parties to intercept and access the data. #### [2. Sign Webhooks for Authenticity and Integrity](#_2-sign-webhooks-for-authenticity-and-integrity) Signing webhooks using a hash-based message authentication code (HMAC) ensures the authenticity and integrity of the messages. HMAC uses a shared secret key between the webhook provider and consumer to create a signature for each message. The consumer can then verify the signature to ensure that the message has not been tampered with during transit. You can view our example of how to sign and verify webhooks using HMAC [here](https://webhookrelay.com/blog/webhook-security/docs/webhooks/auth/hmac/). #### [3. Authenticate Connections to Verify the Source](#_3-authenticate-connections-to-verify-the-source) Authenticating the source of webhook messages is essential to prevent unauthorized access and ensure that requests are coming from the intended source. One common method is to include an authentication token in the webhook request header. The consumer can check for this token to verify the legitimacy of the payload. Additionally, the consumer can whitelist the IP address of the webhook provider to only accept requests from known sources. In Webhook Relay you can do this by going to the bucket details and clicking on authentication tab: ![webhook token authentication](https://webhookrelay.com/blog/webhook-security/images/blog/webhook-security/webhook-token-auth.png) Alternatively you can select "basic" which means a standard username and password authentication will be applied. #### [4. Add Timestamps to Prevent Replay Attacks](#_4-add-timestamps-to-prevent-replay-attacks) Adding timestamps to webhook messages helps prevent replay attacks, where an attacker intercepts and resends a legitimate message at a later time. By including a timestamp in the message and verifying it on the consumer side, the consumer can ensure that the message is current and reject any outdated or replayed messages. Timestamps should be paired with the HMAC check to ensure that the attacker is not just changing the timestamp to a current one. #### [5. Use Certificate Pinning for Server Authentication](#_5-use-certificate-pinning-for-server-authentication) Certificate pinning is a technique used to ensure the authenticity of the server's certificate during the SSL/TLS handshake. By pinning the server's certificate in the code, the consumer can verify that the connection is established with the correct server and prevent attacks with fake or compromised certificates. #### [6. Avoid Sending Sensitive Data Through Webhooks](#_6-avoid-sending-sensitive-data-through-webhooks) Webhooks are typically used for sending notifications about events and are not suitable for transmitting highly sensitive data such as passwords or credit card information. It is recommended to avoid sending sensitive data through webhooks and instead use more secure methods like direct API calls with proper authentication and encryption. A typical workflow here is: 1. GitHub sends a webhook that a push event has happened to a branch 2. CI/CD system receives the webhook and starts a build by cloning the repository from the source In this case, the CI/CD system should not be receiving any sensitive data from the webhook. It should only be receiving the event type and the repository URL. The CI/CD system should then use its own credentials to clone the repository. #### [7. Implement Logging for Auditing and Monitoring](#_7-implement-logging-for-auditing-and-monitoring) Implementing logging for all webhook messages sent out is essential for auditing, monitoring, and detecting security incidents. By logging webhook messages, you can keep a record of every message sent and analyze them for any security-related issues or suspicious activities. When routing webhooks through our platform, you can define either additional outputs (your auditing/logging system) or push webhooks through a function to a [data warehouse](https://webhookrelay.com/blog/webhook-security/docs/tutorials/warehouse/bigquery/). ![webhook audit log in bigquery](https://webhookrelay.com/blog/webhook-security/images/tutorials/functions/whr-to-bigquery.png) #### [8. Use a Subscription Model with Expiration Dates](#_8-use-a-subscription-model-with-expiration-dates) Using a subscription model with expiration dates adds an extra layer of security to your webhook implementation. By allowing users to provide an expiration date for their subscription, you can limit the timeframe for potential attacks. Once the subscription expires, the webhook consumer can stop accepting requests from that particular source. ## [Conclusion](#conclusion) Securing webhooks is crucial to protect against potential vulnerabilities and attacks. By following the best practices outlined in this research report, including encrypting data, signing webhooks, authenticating connections, adding timestamps, using certificate pinning, avoiding sensitive data transmission, implementing logging, and using a subscription model, you can enhance the security of your webhook implementation. It is important to tailor the security measures to the nature of the information being sent and to implement overlapping layers of security for comprehensive protection. Remember that security is an ongoing process, and it is essential to regularly review and update your security measures to stay ahead of potential threats. By prioritizing webhook security, you can ensure the integrity and confidentiality of your data and maintain the trust of your users. ## [Webhook Relay's approach to security](#webhook-relays-approach-to-security) At Webhook Relay all webhooks are encrypted in transit and we provide a number of ways to verify the authenticity of the message. You can monitor, audit and inspect webhook payloads, statuses and your server responses to them. You can use password or HMAC authentication with just a few clicks or implement a custom, provider-specific authentication method with [Functions](https://webhookrelay.com/blog/webhook-security/docs/webhooks/auth/hmac/). ## [References](#references) - [Webhook Security Best Practices](https://snyk.io/blog/creating-secure-webhooks/) - [Webhook Security: How to Secure Webhooks](https://www.elastic.io/integration-best-practices/webhook-security-how-to-secure-webhooks/) - [How to Secure Webhook Endpoints with HMAC](https://prismatic.io/blog/how-secure-webhook-endpoints-hmac/) --- --- title: About Webhook Relay | WebhookRelay meta: "og: title": "About Webhook Relay" description: Webhook Relay is a platform for building and scaling web applications. url: https://webhookrelay.com/about.md file: /about.md --- # **Our story** We are a passionate, dedicated team on a mission to provide a reliable, flexible and high performance webhook routing and transformation system. We value great user experience, quality, innovation, privacy and transparency above everything else. ![About Webhook Relay](https://webhookrelay.com/about/images/dc-network.jpg) ## **Your Problems Are Our Challenges** Our company is not really about us. It's about you and your challenges. We provide tooling that integrates with whatever systems and tools you use. If you would like to see any new feature or just a new module in our serverless functions - please let us know. ### Encryption & Security Webhook Relay tunnels are encrypted. We provide a secure way to deliver webhooks from public services to your private networks. ### Billions of webhooks We have delivered billions of webhooks to our users. Every month we process more than 1 billion webhooks. ### Global regions Webhook Relay tunnels are available across the globe. We are also able to provision custom zones for our customers. ![Stripes](https://webhookrelay.com/about/images/stripes-dark.svg) ## **Start forwarding webhooks in minutes ** Connect a source, pick a destination, and Webhook Relay handles delivery, retries and transforms. Set up your first webhook in under five minutes. [Start for free ->](https://my.webhookrelay.com/register) Free plan · No credit card required · 7-day money-back guarantee on paid plans --- --- title: Webhook Relay - Careers | WebhookRelay meta: "og: title": "Webhook Relay - Careers" description: We're a small team building a reliable, flexible, high-performance webhook routing system. We value user experience, quality, privacy and transparency. url: https://webhookrelay.com/careers.md file: /careers.md --- # **Careers** We are a passionate, dedicated team on a mission to provide a reliable, flexible and high performance webhook routing system. We value great user experience, quality, innovation, privacy and transparency above everything else. ## **Currently open positions: ** ### **- ** Can't see what you are looking for? Drop us an email at info@webhookrelay.com and we will get back to you. --- --- title: Privacy Policy | WebhookRelay meta: "og: title": "Privacy Policy" description: How Webhook Relay collects, processes and protects your personal information on webhookrelay.com, including our GDPR-compliant data processing statement. url: https://webhookrelay.com/privacy.md file: /privacy.md --- ![Stripes](https://webhookrelay.com/privacy/images/stripes.svg) # **Privacy Policy** How Webhook Relay collects, processes and protects your personal information on webhookrelay.com, including our GDPR-compliant data processing statement. ## [Data Processing Statement](#data-processing-statement) In the interest of transparency and [GDPR](https://gdprexplained.eu/) compliance, this page explains how the [Webhook Relay](https://webhookrelay.com) handles and processes your personal information on webhookrelay.com. This is a living document and will be updated with new information as our services evolve. The last updated was June 1st, 2018. You may also be interested in our [Terms of Service](https://webhookrelay.com/tos/) and [Privacy Policy](https://webhookrelay.com/privacy/). ## [The my.webhookrelay.com Website](#the-mywebhookrelaycom-website) Our website uses cookies when you log in to your account. Cookies store a unique token that identifies you. Our website generates standard access- and error logs, which are kept for operational reasons. These logs include IP addresses and information about your web browser and operating system version. They are kept for a maximum of 3 months although usually it's a lot shorter period, and then automatically deleted. ## [Your Account Details](#your-account-details) Your account details; your name, e-mail address and any custom text you have requested we include on your invoices are all visible on your [account page](https://my.webhookrelay.com/account). Note that we do not require our users provide us with real names or custom text, this option is a convenience which allows us to compose friendlier e-mails and make sure your receipts contain the information you need for your own accounting purposes. You are welcome to use a pseudonym or alias. You can also delete your account at any time by using the 'DELETE ACCOUNT' button on that page. The changes made on this page have immediate effect; however old information may remain present in secondary locations, such as: - The internal system audit log - E-mail archives - System backups ## [Data Transmitted Over Webhook Relay Tunnels](#data-transmitted-over-webhook-relay-tunnels) We do not routinely log or store any of the content transmitted over the Webhook Relay tunneling infrastructure. Exceptions to this will be made only temporarily, if necessary to troubleshoot and diagnose system failures, or to comply with legal requirements such as a subpoena. In practice, as of May 2018, this has never happened - we have never had cause to inspect our customers' traffic. However, we reserve the right to do so if it becomes necessary for operational or legal reasons. For operational reasons, our relays keep logs of the following meta-data for up to 7 days: - Number of requests made (also used for rate-limit enforcement) - Bytes transmitted for each tunnel - Tunnel names and protocols used for connections - Dates, times, bytes transmitted and connection speed for each request No backups are kept of relay logs and IP addresses are not collected. Tunnel servers have no access to Account Details and are designed to be standalone. ## [Data Transmitted When Using Webhooks Forwarding](#data-transmitted-when-using-webhooks-forwarding) Webhooks, forwarded through [buckets](https://my.webhookrelay.com/buckets) are recorded in the database (request body, headers and destination) for debugging, auditing or resend purposes. It is stored as long as account subscription plan allows it and then gets automatically deleted. Webhook Relay service only tracks the amount of webhooks forwarded in the current month for billing purposes. Account webhook content data cannot be accessed by anyone, except account owner or any sub-accounts that the account owner has created. ## [Third Parties: Payment Processors](#third-parties-payment-processors) We delegate payment processing to [Stripe](http://stripe.com). Their privacy policies may be found on their respective websites. When a payment is made, our payment processors forward to us the information required for us to validate and process your order. This includes most of the information you have provided them with during the payment process, except for credit card details which we never see. This information is in turn used to provide you with a receipt and update your account quotas. ## [Third Parties: Hosting Providers](#third-parties-hosting-providers) Webhook Relay rents world-wide computing and network capacity from of a variety of different providers, including: - [Google Cloud](https://cloud.google.com/) - main infrastructure provider - [Hetzner](https://www.hetzner.com/cloud/) - stateless tunnel proxy - [Vultr](https://www.vultr.com/) - stateless tunnel proxy - [Cloudflare](http://cloudflare.com/) - DNS provider, website hosting and CDN Most of these servers are tunneling servers which have access to very little personal information and are configured to delete all local logs after only a few days have passed. The exception is our web server and account database servers, which also have the public roles of API and DNS servers. These servers are hosted with Google Cloud and our hosting provider also makes backups of live system data on our behalf. In all cases, the services provided by these Third Parties to Webhook Relay are content agnostic server capacity, public IP addresses and network bandwidth. We do not grant these providers any access to information about individual customers, although by nature of the services they provide, they do store such data on our behalf. We would consider it a breach of trust and a breach of contract if any of these providers were to access our server disks and extract private information about our customers. For any GDPR related queries, feel free to contact us on [info@webhookrelay.com](https://webhookrelay.com/privacy//mailto:info@webhookrelay.com). --- --- title: Webhook Relay - Not Found | WebhookRelay url: https://webhookrelay.com/404.md file: /404.md description: Sorry, the page you are looking for does not exist. --- --- title: "Webhook Relay - Not Found | WebhookRelay" meta: "og:description": "Sorry, the page you are looking for does not exist." "og:title": "Webhook Relay - Not Found" description: "Sorry, the page you are looking for does not exist." --- --- --- title: Enterprise Software License Agreement | WebhookRelay meta: "og: title": "Enterprise Software License Agreement" description: This license is applicable for the self-hosted Webhook Relay server "Transponder". url: https://webhookrelay.com/esla.md file: /esla.md --- ![Stripes](https://webhookrelay.com/esla/images/stripes.svg) # **Enterprise Software License Agreement** This license is applicable for the self-hosted Webhook Relay server "Transponder". ## [1. Definitions](#_1-definitions) For all purposes of this Agreement, the terms defined below, when used with initial capital letters, will have the meanings set forth below. All terms defined elsewhere in this Agreement (including on the Cover Page) will have the meanings ascribed to them as set forth herein. (a) “Confidential Information” means, subject to the exceptions set forth in the following sentence, any information or data, regardless of whether it is in tangible form, disclosed or made available by either party (“Disclosing Party”) that Disclosing Party has either marked as confidential or proprietary, or has identified in writing as confidential or proprietary within thirty (30) days of disclosure to the other party (“Receiving Party”); provided, however, that Disclosing Party’s business plans, strategies, technology, research and development, current and prospective customers, billing records, products and services shall be deemed Confidential Information of Disclosing Party even if not so marked or identified. The Licensor Technology is the Confidential Information of Licensor. Notwithstanding the foregoing to the contrary, information will not be deemed “Confidential Information” if such information: (i) is known to Receiving Party prior to receipt from Disclosing Party directly or indirectly from a source other than one having an obligation of confidentiality to Disclosing Party and without violation of applicable law; (ii) becomes known (independently of disclosure by Disclosing Party) to Receiving Party directly or indirectly from a source other than one having an obligation of confidentiality to Disclosing Party and without violation of applicable law; (iii) becomes publicly known or otherwise ceases to be secret or confidential, except through a breach of this Agreement by Receiving Party; or (iv) is independently developed by Receiving Party without use of or reference to Disclosing Party’s Confidential Information. (b) “Cover Page” means the first page(s) of this Agreement, containing detailed information with respect to Customer and the License and bearing the signatures of the parties. (c) “Documentation” means the user manuals and operator instructions provided by Licensor to Customer in conjunction with the Licensed Software. For the avoidance of doubt, the Documentation includes the Software Specifications. (d) “Effective Date” means the date set forth on the Cover Page. (e) “Force Majeure Event” means an unforeseeable event, or a series of related unforeseeable events, that is outside the reasonable control of the party affected (including without limitation failures of the internet or any public telecommunications network, hacker attacks, denial of service attacks, virus or other malicious software attacks or infections, power failures, industrial disputes affecting any third party, changes to the law, disasters, explosions, fires, floods, riots, terrorist attacks and wars). (f) “License” means the license granted by Licensor to Customer as provided in Section 2(a). (g) “License Fee” means the License Fee set forth on the Cover Page. (h) “Licensed Software” means the proprietary computer software program(s) identified as “Licensed Software” on the Cover Page, (i) including any Updates, (ii) excluding any Open Source Software Linux distribution packaged around the same and (iii) which does not constitute a New Version. (i) “Licensor Technology” means the Licensed Software and the Documentation. (j) “Minimum Term” means the period set out on the Cover Page and beginning on the Effective Date. (k) “New Version” means any new version of the Licensed Software which from time to time is publicly marketed and offered for purchase by Licensor in the course of its normal business, being a version which contains such significant differences from the previous versions as to be generally accepted in the marketplace as constituting a new product. (l) “Open Source Software” means any open source software as defined by the Open Source Initiative ([https://opensource.org/](https://opensource.org/)) of the Free Software Foundation ([https://www.fsf.org/](https://www.fsf.org/)). (m) “Person” means an individual, corporation, partnership, limited liability company, limited liability partnership, syndicate, person, trust, association, organization or other entity. (n) “Software Specifications” means the specifications for the Licensor Software set forth on Licensor’s website ([https://webhookrelay.com](https://webhookrelay.com)) from time to time. (o) “Updates” means updates, upgrades, bug fixes and improvements to the Licensed Software that Licensor makes available to its other customers of the Licensed Software for no additional charge. ## [2. License](#_2-license) (a) License Grant. Subject to the terms and conditions of this Agreement, Licensor hereby grants to Customer a non-exclusive, non-transferable, non-sublicensable limited right and license, during the Term, as follows: - (i) To install and use the Licensed Software in object code form only as delivered pursuant to this Agreement, and consistent with the Documentation, solely for Customer’s internal business purposes; - (ii) To make one (1) additional copy of the Licensed Software and Documentation for back-up or archival purposes; and - (iii) To use the Documentation as reasonably necessary for Customer’s internal business purposes in connection with Customer’s use of the Licensed Software as permitted hereunder. (b) Reserved Rights. Any rights not expressly granted in Section 2(a) are expressly reserved by Licensor. Without limitation of the foregoing, Licensor reserves the right to license the Licensor Technology to others on such terms as Licensor may establish in its sole discretion. Customer acknowledges that no exclusive right of any kind is granted to Customer by the terms of this Agreement. Customer will promptly notify Licensor in writing of any infringement of Licensor’s intellectual property rights that come to the attention of Customer. (c) Limitations and Restrictions. It is expressly understood and agreed that the License is subject to the following limitations and restrictions: - (i) Customer may not use the Licensed Software except for Customer’s internal business purposes in accordance with this Agreement. - (ii) Customer may not use the Licensed Software for an amount of buckets, inputs, outputs, tunnels or other internal service resources, or install the Licensed Software on a number of nodes, exceeding the amounts for each set forth on the Cover Page. - (iii) Customer may not distribute the Licensor Technology, in whole or in part, or any copy thereof, by transfer, lease, loan or any other means, or make it available for use by others in any manner, including without limitation by any time-sharing, service bureau or similar arrangement. - (iv) Customer will not remove, obliterate, obscure, or conceal the proprietary notices or legends which appear on the Licensor Technology, and will reproduce such notices or legends on all copies of the Licensor Technology or of any part of the Licensor Technology. - (v) Customer has no right to obtain or have access to the source code or systems and programming documentation of the Licensed Software or any part thereof. The Licensor Technology and all information related thereto will be subject to Customer’s obligations of confidentiality under Section 9(b). - (vi) Customer may not alter, modify, adapt or create derivative works from the Licensed Software or the Documentation. Customer may not decompile, recompile, disassemble, translate, update, modify, merge, adapt, translate, copy or otherwise reverse engineer the Licensed Software or any part thereof. - (vii) Customer may not use the Licensed Software (A) in violation of applicable local, state, national and international laws and regulations or (B) to create any product or service competitive to the Licensed Software. - (viii) Customer may not share or publish the results of any benchmarking or performance testing, and/or compatibility analysis of the Licensed Software without Licensor’s prior written consent. ## [3. Delivery; Support](#_3-delivery-support) (a) Delivery. Within ten (10) days from the Effective Date, Licensor will make available to Customer for download (a) a copy of the Documentation and (b) a machine-readable copy of the Licensed Software. The Licensor Technology will be deemed accepted by Customer upon download. (b) Support. Licensor will provide Updates to Customer. Customer will install all Updates as soon as reasonably practicable after receipt of the same. Licensor will also perform the support services set forth on Exhibit A attached hereto. Except as otherwise set forth on Exhibit A or in a Statement of Work (as defined below), Customer will be solely responsible for the installation and use of the Licensed Software. ## [4. Professional Services](#_4-professional-services) From time to time, the parties may mutually agree to the performance of certain professional services (“Professional Services”). Any such services will be described on a statement of work that is executed by an authorized representative of each party and expressly references, and is governed by, this Agreement (each, a “Statement of Work”). Professional Services will be provided in accordance with the provisions of this Agreement and the applicable Statement of Work. Each Statement of Work will contain a description of the tasks to be performed, a schedule of payments and payment terms (if applicable) and any additional terms and conditions the parties wish to include. Licensor will exercise due skill, care and diligence in the performance of the Professional Services set forth in a Statement of Work and will carry out its activities in a professional and workmanlike manner, using appropriately qualified, skilled and experienced personnel as can be reasonably expected given the scope, type, nature and complexity of the services described in the applicable Statement of Work. In the event of a conflict between any term or condition of this Agreement and any Statement of Work, the applicable term or condition of this Agreement will govern unless and solely to the extent that the parties expressly state in such Statement of Work that they intend to override the terms and conditions of this Agreement. Upon execution of any Statement of Work, the terms and conditions of such Statement of Work are hereby incorporated into and become part of this Agreement. ## [5. Fees](#_5-fees) (a) License Fee. In consideration of the License granted under Section 2(a), Customer will pay to Licensor the License Fee. Unless otherwise agreed in writing by the parties, for each Renewal Term (as defined below) the License Fee payable will be the then-prevailing rate for the license for the Licensed Software for the applicable Renewal Term at the tier and capacity set forth on the Cover Page or as otherwise agreed by the parties in writing at least ninety (90) days prior to the beginning of the applicable Renewal Term. (b) Payment. The License Fee will be due and payable by electronic / wire transfer in accordance with Licensor’s instructions, thirty (30) days after the Effective Date and the start of the applicable Renewal Term. In addition, Customer will pay to Licensor the fees for any Professional Services as set forth in the applicable Statement of Work. Customer will pay the License Fee and any fees for Professional Services in U.S. Dollars. (c) Taxes. Customer will be responsible for the payment of all sales, use, personal property or other taxes arising from or relating to the License, the receipt of Professional Services, or Customer’s possession and use of the Licensor Technology, other than taxes based on or measured by Licensor’s net income. Customer will pay such taxes directly or, if Licensor pays or is required to pay any such taxes, Customer will promptly reimburse Licensor therefor. (d) Finance Charge. Any payment not made when due hereunder will be subject to a finance charge in the amount of one and one-half percent (1.5%) for each month or part of a month that payment is overdue, but not greater than the highest rate of interest allowed by applicable law, however, nothing herein will limit Licensor’s right to terminate this Agreement under Section 10(b) hereof. (e) Costs of Collection. In the event that Licensor retains a collection agent or undertakes legal action to collect amounts not paid by Customer when due under this Agreement, Customer will reimburse Licensor for all reasonable costs incurred (including reasonable attorneys' fees) in collecting past due amounts. ## [6. Other Obligations of Customer](#_6-other-obligations-of-customer) (a) Installation, Maintenance and Other Services. Except as otherwise provided in a Statement of Work or separate service or maintenance agreement between the parties, Customer will be solely responsible for its installation and use of the Licensed Software, and Licensor will have no obligation or responsibility with respect thereto. (b) Recordkeeping and Access. Customer agrees to maintain reasonable records with respect to its installation and use of the Licensed Software, including without limitation records identifying the computer, server or other device on which the Licensed Software is installed from time to time and identifying all individuals having access to the Licensed Software, and to retain all such records for a period of at least three (3) years from the date of their creation. Customer will make such records and any computer, server or other device on which the Licensed Software is installed available for inspection by Licensor or its representatives at any time during normal business hours upon request. Customer will promptly pay Licensor additional license fees at Licensor’s then-current rates, and related finance charges as provided in Section 5(d), for any reproduction, installation, use or distribution of the Licensor Technology in excess of the rights conferred under the License. Customer will also pay Licensor its expenses for any such inspection which discloses underpayment of amounts due to Licensor greater than ten percent (10%) of the License Fee payable under this Agreement. Customer also acknowledges and agrees that Licensor may monitor Customer’s usage of the Licensed Software and may collect data related to such usage (including with respect to the technical integration and configuration). Licensor retains all rights, title and interest in and to such data and will use such data in accordance with its Privacy Policy located at [https://webhookrelay.com/privacy/](https://webhookrelay.com/privacy/), as may be amended by Licensor from time to time. (c) Notice of Certain Events. Customer agrees to give prompt written notice to Licensor if at any time Customer becomes aware that any person has received, used, accessed or had access to any of Licensor’s Confidential Information in violation of Licensor’s rights therein. ## [7. Warranties](#_7-warranties) (a) Each of Licensor and Customer warrants to the other that it has the legal right and authority to enter into this Agreement and to perform its obligations under this Agreement. Licensor warrants that, to the best of Licensor’ knowledge, it has sufficient right and authority to grant to Customer all licenses and rights granted under this Agreement. (b) Licensor further grants to Customer a limited warranty that it will use commercially reasonable efforts to ensure that the Licensed Software (a) is provided free from viruses, worms, Trojan horses, ransomware, spyware, adware and other malicious software programs, (b) incorporates security features reflecting the requirements of good industry practice and (c) will function materially in conformance with the specifications set forth in the Documentation. (c) The warranties contained in Section 7(b) are subject to the following qualifications: - (i) Licensor does not warrant that the Licensed Software will meet the Customer’s requirements or expectations, or that the Licensed Software will operate uninterrupted or error-free, or that any or all Licensed Software errors will be corrected or are correctable. - (ii) Customer acknowledges that complex software is never entirely free from security vulnerabilities; and subject to the other provisions of this Agreement, Licensor gives no warranty or representation that the Licensed Software will be entirely secure. - (iii) Customer acknowledges that the Licensed Software is only designed to be compatible with that software specified as compatible in the Software Specifications; and Licensor does not warrant or represent that the Licensed Software will be compatible with any other software. - (iv) Customer acknowledges that any Open Source Software provided by Licensor is provided “as is” and expressly subject to the disclaimer in Section 7(e). - (v) Customer acknowledges that Licensor will not provide any legal, compliance, financial, accountancy or taxation advice under this Agreement or in relation to the Licensed Software; and, except to the extent expressly provided otherwise in this Agreement, Licensor does not warrant or represent that the Licensed Software or the use of the Licensed Software by Customer will not give rise to any legal liability on the part of Customer or any other Person. (d) For any breach of the warranty contained in this Section, Customer’s sole and exclusive remedy, and Licensor’s entire liability and obligation will be, at Licensor’s election, to repair or replace the Licensed Software, or if Licensor is unable to repair or replace the Licensed Software after using commercially reasonable efforts, to refund the fees paid less the portion of the license fees attributable to the period over which Customer actually used the Licensed Software, assuming full amortization of the Licensed Software over a period of three (3) years. (e) Other than as expressly set forth above, Licensor does not make any express or implied warranties, conditions, or representations to Customer or any other party with respect to the Licensed Software or any services provided hereunder or otherwise regarding this Agreement, whether oral or written, express, implied, or statutory. Without limiting the foregoing, any implied warranty or condition of merchantability, title, noninfringement, or fitness for a particular purpose are expressly excluded and disclaimed. ## [8. Intellectual Property Rights indemnity](#_8-intellectual-property-rights-indemnity) (a) By Licensor. Licensor will defend or settle any third-party suit or proceeding brought against Customer based upon a claim that Customer’s use of the Licensed Software as permitted under this Agreement constitutes an infringement of any existing and valid copyright, patent, trademark or trade secret (any such suit or proceeding, a “Claim”); provided that Customer (i) promptly notifies Licensor in writing of such claim, (ii) promptly gives Licensor the right to control and direct the investigation, preparation, defense and settlement of such Claim with counsel of Licensor’s own choosing (provided that Customer will have the right to reasonably participate, at its own expense, in the defense or settlement of any such Claim), and (iii) gives assistance and full cooperation for the defense of same. Subject to Customer’s compliance with the foregoing requirements, Licensor will pay all liabilities, damages, losses, costs and expenses awarded by a court of competent jurisdiction against Customer in such Claim or amounts payable pursuant to a settlement agreed to by Licensor, but will not be responsible for any cost, expense or compromise incurred or made by Customer without Licensor’s prior written consent or for any lost profits or other damage or loss suffered by Customer. Any compromise or settlement of any such Claim will require the prior written consent of both parties, such consent not to be unreasonably withheld, conditioned or delayed. Notwithstanding the foregoing, the foregoing defense and indemnity obligations will not apply to any Claim based upon or arising from (A) use of the Licensed Software in a manner for which it was not designed or not in accordance with applicable Documentation, (B) any modification of the Licensed Software by any party other than Licensor, (C) any use of the Licensed Software in combination with hardware or software not provided or authorized by Licensor, or (D) use of the Licensed Software, when use of a subsequent Update which Licensor has made commercially available would have avoided such infringement. (b) By Customer. Except with respect to matters for which Licensor is liable pursuant to Section 8(a), Customer hereby agrees to indemnify Licensor and its affiliates, and hold it and them harmless, against any liabilities, damages, losses, costs and expenses that may be suffered by Licensor, and any claims that may be made against Licensor or any Licensor affiliate by any Person, as a direct or indirect result of the use by Customer of the Licensor Technology, or of any breach of this Agreement by Customer; provided that Licensor (i) promptly notifies Customer in writing of such claim (ii) promptly gives Customer the right to control and direct the investigation, preparation, defense and settlement of such claim with counsel of Customer’s own choosing (provided that Customer will have the right to reasonably participate, at its own expense, in the defense or settlement of any such claim); and (iii) gives assistance and full cooperation for the defense of same. Subject to Licensor’s compliance with the foregoing requirements, Customer will pay all liabilities, damages, losses, costs and expenses awarded by a court of competent jurisdiction against Licensor in such claim or amounts payable pursuant to a settlement agreed to by Customer, but will not be responsible for any cost, expense or compromise incurred or made by Licensor without Customer’s prior written consent or for any lost profits or other damage or loss suffered by Customer. Any compromise or settlement of any such claim will require the prior written consent of both parties, such consent not to be unreasonably withheld, conditioned or delayed. (c) Additional Infringement Remedy. If any Licensor Technology is in the opinion of Licensor likely to, or does, become the subject of a claim of infringement, Licensor may, at its sole option, procure for Customer the right to continue using the Licensor Technology, modify the affected Licensor Technology to become non-infringing, or replace it with non-infringing Licensor Technology. If Licensor is not reasonably able to so modify or replace the Licensor Technology or otherwise secure for Customer the right to continue using the Licensor Technology, Licensor may terminate this Agreement and, upon return to it of all copies of the Licensed Software and Documentation licensed hereunder, refund to Customer the amount of the License Fee actually paid which relates pro-rata to the remaining period of the then-current term. (d) Limitation of Liability. - (i) EXCEPT AS EXPRESSLY PROVIDED IN SECTION 8, LICENSOR’S AND ITS AFFILIATES’ SOLE LIABILITY, AND CUSTOMER’S SOLE AND EXCLUSIVE REMEDY, FOR ANY BREACH OF WARRANTY OR OTHER MATTER ARISING UNDER OR RELATED TO THIS AGREEMENT WILL BE THE REMEDIES AVAILABLE PURSUANT TO LICENSOR’S WARRANTY IN SECTION 7. - (ii) NOTWITHSTANDING ANY OTHER PROVISION OF THIS AGREEMENT, EXCEPT FOR LIABILITY ARISING FROM (A) CLIENT’S USE OF THE LICENSED SOFTWARE OTHER THAN EXPRESSLY PERMITTED BY SECTION 2 (LICENSE), (B) EITHER PARTY’S BREACH OF SECTION 9 (PROPRIETARY RIGHTS AND CONFIDENTIALITY), AND (C) A PARTY’S INDEMNIFICATION OBLIGATIONS SET FORTH IN SECTION 8 (INDEMNIFICATION), UNDER NO CIRCUMSTANCES WILL EITHER PARTY’S LIABILITY FOR ALL CLAIMS ARISING UNDER OR RELATING TO THIS AGREEMENT (INCLUDING BUT NOT LIMITED TO WARRANTY CLAIMS), REGARDLESS OF THE FORUM AND REGARDLESS OF WHETHER ANY ACTION OR CLAIM IS BASED ON CONTRACT, TORT, OR OTHERWISE, EXCEED THE AGGREGATE FEES PAID BY CUSTOMER TO LICENSOR UNDER THIS AGREEMENT DURING THE TWELVE (12) MONTH PERIOD PRECEDING THE EVENT OR CIRCUMSTANCES GIVING RISE TO SUCH LIABILITY. THIS LIMITATION OF LIABILITY IS CUMULATIVE AND NOT PER INCIDENT. - (iii) THE PARTIES HERETO AGREE THAT, NOTWITHSTANDING ANY OTHER PROVISION IN THIS AGREEMENT, EXCEPT FOR LIABILITY ARISING FROM (A) CLIENT’S USE OF THE LICENSED SOFTWARE OTHER THAN EXPRESSLY PERMITTED BY SECTION 2 (LICENSE), (B) EITHER PARTY’S BREACH OF SECTION 9 (PROPRIETARY RIGHTS AND CONFIDENTIALITY), AND (C) A PARTY’S INDEMNIFICATION OBLIGATIONS SET FORTH IN SECTION 8 (INDEMNIFICATION), IN NO EVENT WILL EITHER PARTY BE LIABLE TO THE OTHER FOR ANY SPECIAL, INDIRECT, RELIANCE, INCIDENTAL OR CONSEQUENTIAL DAMAGES OF ANY KIND, LOST OR DAMAGED DATA, LOST PROFITS OR LOST REVENUE, WHETHER ARISING IN CONTRACT, TORT (INCLUDING NEGLIGENCE), OR OTHERWISE, EVEN IF A PARTY HAS BEEN NOTIFIED OF THE POSSIBILITY THEREOF. ## [9. Proprietary Rights and Confidentiality](#_9-proprietary-rights-and-confidentiality) (a) Proprietary Rights. The Licensor Technology and all proprietary rights therein, including, without limitation, any and all intellectual property rights with respect to any of the Licensor Technology, and all improvements, enhancements or modifications thereto, or derivatives thereof, are and will be and remain at all times the property of Licensor, and Customer will have no right, title or interest therein except as expressly provided herein. (b) Confidentiality. Receiving Party acknowledges that the Confidential Information constitutes valuable trade secrets and proprietary information of Disclosing Party, and Receiving Party agrees that (i) it shall use the Confidential Information of Disclosing Party solely to perform its obligations or exercise its rights under this Agreement and (ii) it will not disclose or make available, or permit to be disclosed or made available, the same directly or indirectly, to any third party without Disclosing Party’s prior written consent, except as otherwise permitted hereunder. Nothing in this Section 9(b) is intended to restrict or otherwise limit the exercise by a party of the rights and licenses granted to it under this Agreement; provided that Receiving Party uses reasonable measures to protect the confidentiality and value of Disclosing Party’s Confidential Information. Notwithstanding any provision of this Agreement, Receiving Party may disclose Disclosing Party’s Confidential Information, in whole or in part (A) to Receiving Party’s employees, independent contractors, officers and directors, and its professional advisors (e.g., attorneys and accountants) who have a need to know and are legally bound by written agreements, or in the case of professional advisors ethical obligations, that contain enforceable confidentiality and use restrictions consistent with those of this Agreement; and (B) as required by law (in which case Receiving Party shall provide Disclosing Party with prior written notification thereof, shall provide Disclosing Party with the opportunity to contest such disclosure, and shall use its reasonable efforts to minimize such disclosure to the extent permitted by applicable law). Receiving Party agrees to exercise due care in protecting Disclosing Party’s Confidential Information from unauthorized use and disclosure. In the event of actual or threatened breach of the provisions of this Section 9(b), Disclosing Party will be entitled to seek immediate injunctive and other equitable relief, without waiving any other rights or remedies available to it. Receiving Party shall promptly notify Disclosing Party in writing if it becomes aware of any violations of the confidentiality obligations set forth in this Agreement. Upon the termination of this Agreement, Receiving Party agrees to promptly return to Disclosing Party or destroy all Confidential Information of Disclosing Party that is in the possession of Receiving Party and to certify the return or destruction of all such Confidential Information and embodiments. ## [10. Term and Termination](#_10-term-and-termination) (a) Term. The initial term of this Agreement will commence on the Effective Date and extend until the end of the Minimum Term, at which point this Agreement will automatically renew for subsequent periods of equal length to the Minimum Term (each, a “Renewal Term”, and together with the Minimum Term, the “Term”), unless either party provides written notice to the other party of its intent to not renew this Agreement at least ninety (90) days prior to the end of the initial term or the then-current Renewal Term, except in the event of the earlier termination of this Agreement in accordance with its terms. All Statements of Work will terminate upon the expiration or earlier termination of this Agreement. (b) Termination by Licensor. Licensor may terminate this Agreement and/or any Statement Work by written notice to Customer, in the event of the occurrence of any of the following: (i) if Customer violates the confidentiality provisions of Section 9(b) of this Agreement, or uses, reproduces, distributes or sublicenses any of the Licensed Software or the Documentation in any manner not authorized by the License granted herein; (ii) if Customer fails to observe or perform any term or condition of this Agreement (other than as set forth in clause (i)) and does not cure such failure within thirty (30) days after written demand by Licensor; or (iii) if Customer makes a general assignment for the benefit of creditors, or files a voluntary petition in bankruptcy or for reorganization or arrangement under the bankruptcy laws, or if a petition in bankruptcy is filed against Customer and is not dismissed within thirty (30) days after the filing, or if a receiver or trustee is appointed for all or any part of the property or assets of Customer. (c) Termination by Customer. Customer may terminate this Agreement and/or any Statement of Work by written notice to Licensor, if Licensor fails to observe or perform any term or condition of this Agreement and does not cure such failure within ninety (90) days after written demand by Customer. (d) Rights and Obligations on Termination. Upon the expiration or termination of this Agreement, (i) the License granted under this Agreement will terminate, and Customer will immediately cease all use of the Licensor Technology and (ii) Receiving Party will, at Disclosing Party’s option, return to Disclosing Party all copies, notes, memoranda and other tangible embodiments of Disclosing Party’s Confidential Information in its possession or under its control, or destroy all such tangible embodiments and certify such destruction in writing to Disclosing Party. Upon such expiration or termination, all rights and obligations of the parties under this Agreement will cease, except that Customer will remain obligated to make any payment due under this Agreement and payment for any fees that accrued under any Statement of Work prior to the date of such expiration or termination. Notwithstanding anything to the contrary herein, the following Sections will survive any expiration or termination of this Agreement: Sections 1, 5, 6(b), 7(3), 8, 9, 10(d) and 11. (e) The termination of this Agreement or any license will not limit either party from pursuing any other remedies available to it, including injunctive relief. ## [11. General](#_11-general) (a) **Entire Agreement**. This Agreement (including any Statements of Work) contains the entire understanding of the parties about its subject. It supersedes and replaces all prior or contemporaneous understandings or agreements, written or oral, regarding such subject matter, and prevails over any conflicting terms or conditions contained on printed forms submitted with purchase orders, sales acknowledgments or quotations. (b) **Amendment**. This Agreement and each Statement of Work may not be amended or modified, in whole or part, except by a writing signed by duly authorized representative of both parties. (c) **Waivers**. No provision or part of this Agreement or remedy hereunder may be waived except by a writing signed by a duly authorized representative of the party making the waiver. Any such waiver will be narrowly construed to apply only to the specific provision and under the specific circumstances for which it was given, and will not apply with respect to any repeated or continued violation of the same provision or any other provision. Failure or delay by either party to enforce any provision of this Agreement will not be deemed a waiver of future enforcement of that or any other provision. (d) **Relationship** of Parties. Nothing in this Agreement will be construed to place Licensor and Customer in an agency, employment, franchise, joint venture, or partnership relationship. Neither party will have the authority to obligate or bind the other in any manner, and nothing herein contained will give rise or is intended to give rise to any rights of any kind to any third parties. Neither party will represent to the contrary, either expressly, implicitly or otherwise. (e) **Assignment**. Neither party may assign this Agreement without the consent of the other party; provided, that, Licensor may assign this Agreement without such consent to (a) its affiliate or (b) a successor to all or substantially all of the assets or business of Licensor to which this Agreement relates, whether by merger, purchase or otherwise. Any attempt by either party to assign or transfer this Agreement without the other party’s consent will be null and void. (f) **Severability**. In the event that any provision of this Agreement is found to be unenforceable, such provision will be reformed only to the extent necessary to make it enforceable, and such provision as so reformed will continue in effect, to the extent consistent with the intent of the parties as of the Effective Date. If any provision or part of this Agreement will, to any extent, be or become invalid, illegal or unenforceable, the remainder of this Agreement will continue in effect, and every other provision of this Agreement will remain valid and enforceable to the full extent permitted by applicable law. In such event, the invalid or unenforceable provision will be reformed only to the extent necessary to make it enforceable, and such provision as so reformed will continue in effect, to the extent consistent with the intent of the parties as of the Effective Date. (g) **Notices**. All notices under or related to this Agreement will be in writing and will reference this Agreement. Notices will be deemed given when: (i) delivered personally; (ii) sent by email or other electronic means, with receipt confirmed in writing (email to suffice) by the recipient; (iii) five (5) days after having been sent by registered or certified mail, return receipt requested, postage prepaid; or (iv) one (1) day after deposit with a commercial overnight carrier, with written verification of receipt. All notices sent electronically / via email from the Customer to the Licensor will be sent to an email address [info@webhookrelay.com](https://webhookrelay.com/esla//mailto:info@webhookrelay.com). All other communications will be sent to the addresses set forth on the Cover Page or such other addresses designated pursuant to this Section 11(g). (h) **Force Majeure**. Neither party will be in breach of this Agreement nor liable for delay in performing, or failure to perform, any of its obligations under this agreement if such delay or failure result from a Force Majeure Event affecting such party. In such circumstances the time for performance will be extended by a period equivalent to the period during which performance of the obligation has been delayed or failed to be performed. If the period of delay or non-performance continues for ninety (90) days, the unaffected party may terminate this Agreement by giving ten (10) days’ written notice to the affected party. (i) **Publicity**. Neither party will disclose the terms of this Agreement without prior written consent from the other party. Customer, however, agrees that its name and any logo may be included by Licensor in any published list of Licensor’s licensees on its website and in its other marketing materials. (j) **Governing Law**. This Licence and any dispute or claim arising out of or in connection with it or its subject matter or formation (including non-contractual disputes or claims) shall be governed by and construed in accordance with the law of England and Wales. (k) **Jurisdiction**. The parties irrevocably agree that the courts of England and Wales shall have exclusive jurisdiction to settle any dispute or claim that arises out of or in connection with this agreement or its subject matter or formation (including non-contractual disputes or claims). (l) **Counterparts**. This Agreement may be executed in two or more counterparts, each of which will be deemed an original, but all of which will constitute but one and the same instrument. ## [EXHIBIT A](#exhibit-a) ### [Support Services](#support-services) Support services consist of Error Correction and Online Support. Customer may submit a ticket for support services by sending an e-mail to [support@webhookrelay.com.com](https://webhookrelay.com/esla//mailto:support@webhookrelay.com.com) Licensor will (a) respond to any Online Support request within one (1) business day of receipt and (b) exercise commercially reasonable efforts to correct any Error reported by Customer in the current release of Licensed Software by providing a Workaround or Fix. For purposes of this Exhibit A, the above capitalized words will have the following meanings ascribed to them: - “Error” means an error in Licensed Software which significantly degrades the Licensed Software as compared to Licensor’s published performance specifications. - “Error Correction” means the use of commercially reasonable efforts to correct Errors. - “Fix” means the repair or replacement of object or executable code versions of Licensed Software to remedy an Error. - “Online Support” means technical support assistance provided by Licensor to Customer during normal business hours concerning the installation and use of Licensed Software. - “Workaround” means a change in the procedures followed or data supplied by Customer to avoid an Error without substantially impairing Customer’s use of Licensed Software. --- --- title: Bin Deleted | WebhookRelay meta: "og: title": "Bin Deleted" description: Bin deleted url: https://webhookrelay.com/bin-deleted.md file: /bin-deleted.md --- # **Webhook Bin Deleted** Your webhook bin and all its contents have been deleted. [Create New Bin](https://webhookrelay.com/bin-deleted/webhook-bin/) --- --- title: Webhook Relay - Contact Sales | WebhookRelay meta: "og: title": "Webhook Relay - Contact Sales" description: Contact our sales team to discuss your enterprise needs and get a custom quote for Webhook Relay. url: https://webhookrelay.com/contact-sales.md file: /contact-sales.md --- # **Contact Us ** Get in touch with our team to discuss your enterprise needs. --- --- title: Free HMAC Generator & Signature Verifier | WebhookRelay meta: "og: title": "Free HMAC Generator & Signature Verifier" description: Free online HMAC generator and webhook signature verifier. Create or check an HMAC (SHA256, SHA512, SHA1, MD5) from a payload and secret. No signup. url: https://webhookrelay.com/hmac-verification.md file: /hmac-verification.md --- # **HMAC Generator & Webhook Signature Verifier** Free online HMAC generator and webhook signature verifier. HMAC (Hash-based Message Authentication Code) is the most popular way to authenticate and secure webhook requests. Paste your payload and secret to generate an HMAC signature (SHA256, SHA512, SHA1 or MD5), or check a computed hash against the signature a provider sent — the same check you’d run to verify **GitHub**, **Stripe**, **Shopify** or **Slack** webhooks. Everything runs without signup. **Algorithm** **Paste request body here** **Webhook secret *** **Computed HMAC signature** **Expected HMAC signature (optional)** Learn how to [verify webhook signatures with HMAC](https://webhookrelay.com/hmac-verification/docs/webhooks/auth/hmac) automatically with Webhook Relay, or [test webhooks online](https://webhookrelay.com/hmac-verification/webhook-bin/) with our free Webhook Bin. ## **Verify a specific provider’s webhook** These verifiers bake in each provider’s exact signing scheme (header, timestamp and encoding) — including the [Standard Webhooks](https://webhookrelay.com/hmac-verification/verify-standard-webhooks-signature/) spec used by OpenAI, Anthropic and Svix. [Stripe](https://webhookrelay.com/hmac-verification/verify-stripe-webhook-signature/) [GitHub](https://webhookrelay.com/hmac-verification/verify-github-webhook-signature/) [Shopify](https://webhookrelay.com/hmac-verification/verify-shopify-webhook-signature/) [Slack](https://webhookrelay.com/hmac-verification/verify-slack-webhook-signature/) [Twilio](https://webhookrelay.com/hmac-verification/verify-twilio-webhook-signature/) [Standard Webhooks](https://webhookrelay.com/hmac-verification/verify-standard-webhooks-signature/) [All providers →](https://webhookrelay.com/hmac-verification/verify-webhook-signature/) --- --- title: Debug Webhooks: A Practical Troubleshooting Guide (2026) meta: "og: title": "Debug Webhooks: A Practical Troubleshooting Guide (2026)" description: Troubleshoot webhooks step by step: events that never arrive, signature mismatches, content-type and body parsing, duplicates, timeouts and retries. url: https://webhookrelay.com/blog/how-to-debug-webhooks.md file: /blog/how-to-debug-webhooks.md --- ![Stripes](https://webhookrelay.com/blog/how-to-debug-webhooks/images/stripes.svg) # **Debug Webhooks: A Practical Troubleshooting Guide (2026)** Troubleshoot webhooks step by step: events that never arrive, signature mismatches, content-type and body parsing, duplicates, timeouts and retries. A webhook that "doesn't work" almost always fails at one of two points: **it never reaches your handler**, or **it reaches your handler and the handler chokes on it**. The trick to debugging webhooks quickly is figuring out which of those two it is — and then you only have a handful of likely causes to check. This guide walks the failure modes in order, with the fastest way to confirm each one. If you haven't already, the companion piece on [how to test webhooks](https://webhookrelay.com/blog/how-to-debug-webhooks/blog/how-to-test-webhooks/) covers the setup; here we focus on what to do when something's broken. ## [First: is it arriving at all?](#first-is-it-arriving-at-all) Before you touch your code, find out whether the request is even leaving the provider. Most providers keep a delivery log — Stripe, GitHub and Shopify all show every attempt and the HTTP response they received. Look there first. **The URL is wrong.** This is the boring, common one. A trailing slash, the wrong path (`/webhook` vs `/webhooks`), `http` instead of `https`, or a stale endpoint from a previous environment. Copy the URL straight from your dashboard and compare it character for character with what the provider has saved. **The provider isn't configured to send the event.** Many providers only deliver the event types you subscribe to. If you wired up `payment_intent.succeeded` but you're triggering a `checkout.session.completed`, nothing arrives — and that's correct behaviour, not a bug. Confirm the event type is enabled. **A firewall or private network is blocking it.** If your endpoint lives on `localhost`, a private LAN, or behind a corporate firewall, the provider physically can't reach it. Public providers can only call public URLs. This is exactly what [Webhook Relay](https://webhookrelay.com) solves: it exposes a stable public endpoint and the [relay agent](https://webhookrelay.com/blog/how-to-debug-webhooks/webhooks/) forwards each request inbound to your private destination, no open ports required. **Confirm the request leaves the provider.** Point the provider at a free [Webhook Bin](https://webhookrelay.com/blog/how-to-debug-webhooks/webhook-bin/) and trigger the event. If it shows up in the bin, the provider is sending correctly and the problem is downstream (your URL, server or firewall). If it _doesn't_ show up in the bin, the problem is on the provider's side — wrong event subscription, disabled endpoint, or an account/auth issue. ## [It's arriving but failing](#its-arriving-but-failing) Now the request is reaching you and something downstream rejects it. Inspect the **exact** request first — in a Webhook Bin or your Webhook Relay request log — so you're debugging facts, not assumptions. ### [Signature mismatch](#signature-mismatch) If your handler returns 401/400 on a "valid" webhook, signature verification is the usual suspect. The number-one cause: **you're verifying against a parsed-and-re-serialized body instead of the raw bytes.** Re-encoding JSON changes whitespace and key ordering, and the HMAC no longer matches. Checklist: - Verify against the **raw request body**, exactly as received. - Use the **correct signing secret** (the per-endpoint secret, not your API key — they're often different). - Read the **right header**. GitHub sends `X-Hub-Signature-256` (SHA256); Stripe and Shopify also use SHA256 but format the header differently. Walk through the mechanics in [how to verify a webhook signature](https://webhookrelay.com/blog/how-to-debug-webhooks/blog/verify-webhook-signature/), and use the free [HMAC generator & verifier](https://webhookrelay.com/blog/how-to-debug-webhooks/hmac-verification/) to paste the raw body, the secret and the header value and confirm they actually match — that isolates whether the problem is your verification code or the data feeding it. ### [Wrong content-type](#wrong-content-type) Your framework picks a body parser based on the `Content-Type` header. If the provider sends `application/x-www-form-urlencoded` (Twilio, some Slack endpoints) but your code assumes JSON — or vice versa — you'll get an empty object or a parse error even though the body is perfectly fine. Check the header in the captured request and match your parser to it. ### [Body parsing](#body-parsing) Even with the right content-type, the structure may not be what you expect. Some providers wrap the real payload in an envelope (`{ "data": { "object": {...} } }`), send a JSON string inside a form field, or batch multiple events into an array. Inspect a real captured payload before writing field accessors, and guard against missing keys instead of assuming a shape. ## [Duplicates and out-of-order events](#duplicates-and-out-of-order-events) Webhook delivery is **at-least-once**, not exactly-once. Two realities follow: - **Duplicates.** A provider that times out waiting for your `2xx` will retry — even if your handler actually succeeded. Make handlers **idempotent**: key off the event ID (or a hash of the payload) and skip anything you've already processed. - **Ordering.** Events can arrive out of order. A `subscription.updated` can land before the `subscription.created`. Don't assume sequence; reconcile against the provider's current state, or fetch the latest object by ID when order matters. ## [Timeouts and retries](#timeouts-and-retries) Most providers give your endpoint a short window (often a few seconds) to respond, and treat anything that's slow or non-`2xx` as a failure to retry. - **Respond fast, work later.** Acknowledge with `2xx` immediately, then do heavy lifting (DB writes, third-party calls) asynchronously. A handler that does all its work before responding is the most common cause of phantom retries and duplicates. - **Return the right status.** A `500` invites retries; a `400` may make the provider give up. Return `2xx` once you've safely accepted the event. ## [The debugging workflow](#the-debugging-workflow) A loop that makes all of the above fast: 1. **Inspect** — send the webhook to a [Webhook Bin](https://webhookrelay.com/blog/how-to-debug-webhooks/webhook-bin/) to capture the exact method, headers, query and body. Now you know what's really being sent. 2. **Forward to localhost** — run the [relay agent](https://webhookrelay.com/blog/how-to-debug-webhooks/webhooks/) and forward into your dev environment so you can set breakpoints in your IDE against live events:``` relay forward --bucket my-app http://localhost:8080/webhook ``` The agent connects outbound, so there's nothing to expose and the public URL stays stable while you iterate. 3. **Replay** — capture a request once and resend it as many times as you need. You re-run the handler without re-triggering the source event (no need to create another payment to test the `payment.succeeded` path). Inspect, forward, replay — repeat until the handler is green. ## [Common pitfalls, at a glance](#common-pitfalls-at-a-glance) - Verifying the signature against a re-serialized body instead of the raw bytes. - Assuming JSON when the provider sent form-encoded (check `Content-Type`). - Doing slow work before responding, triggering retries and duplicates. - Non-idempotent handlers that double-process retried events. - A stale or private endpoint URL the provider can't actually reach. ## [Start debugging](#start-debugging) [Open a Webhook Bin](https://webhookrelay.com/blog/how-to-debug-webhooks/webhook-bin/) to see the exact request a provider is sending, or [create a free account](https://my.webhookrelay.com/register) to forward live webhooks into your IDE and replay them against your handler until it works. --- --- title: Webhook Authentication: HMAC, mTLS & Tokens | WebhookRelay meta: "og: title": "Webhook Authentication: HMAC, mTLS & Tokens" description: A practical guide to webhook authentication: HMAC signatures, shared-secret tokens, basic auth, mTLS, IP allow-listing and JWT, and when to use each. url: https://webhookrelay.com/blog/webhook-authentication.md file: /blog/webhook-authentication.md --- ![Stripes](https://webhookrelay.com/blog/webhook-authentication/images/stripes.svg) # **Webhook Authentication: HMAC, Tokens, mTLS and IP Allow-listing Compared** A practical guide to webhook authentication: HMAC signatures, shared-secret tokens, basic auth, mTLS, IP allow-listing and JWT, and when to use each. A webhook endpoint is a public URL that accepts POST requests and does something consequential — creates orders, deploys code, moves money. If anyone on the internet can forge a request your handler trusts, that's a problem. **Webhook authentication** is how you prove an incoming request really came from the provider you expect, and that nobody altered it on the way. There isn't one right method; there are several, with different guarantees and costs. This guide walks through each, compares them, and tells you when to reach for which. ## [The strategies, from most to least common](#the-strategies-from-most-to-least-common) ### [1. HMAC signatures (the default for a reason)](#_1-hmac-signatures-the-default-for-a-reason) The dominant approach. The provider and you share a secret. For every webhook, the provider computes an **HMAC** (a keyed hash, usually SHA-256) over the raw request body and puts the result in a header — `Stripe-Signature`, `X-Hub-Signature-256` (GitHub), `X-Shopify-Hmac-Sha256`, and so on. You recompute the HMAC with the same secret and compare. ``` import hmac, hashlib def verify(raw_body: bytes, header_sig: str, secret: str) -> bool: expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, header_sig) # constant-time! ``` Why it wins: - **Proves authenticity _and_ integrity** — only someone with the secret could produce the signature, and any change to the body breaks it. - **The secret never travels** on the wire; only the derived signature does. - Works over plain HTTPS with no client certs or infrastructure. Two rules that trip people up: **verify against the raw bytes**, not a re-serialized JSON object (re-encoding changes bytes and breaks the match), and **use a constant-time comparison** to avoid timing attacks. Many providers also include a timestamp you should check to reject replays. We cover the mechanics in depth in [how to verify a webhook signature](https://webhookrelay.com/blog/webhook-authentication/blog/verify-webhook-signature/), and you can test your logic with the free [HMAC generator & verifier](https://webhookrelay.com/blog/webhook-authentication/hmac-verification/). ### [2. Shared-secret tokens](#_2-shared-secret-tokens) The simplest scheme: the provider sends a secret value in a header (e.g. `Authorization: Bearer ` or a custom `X-Webhook-Token`), and you compare it to a known value. Easy to set up, but weaker than HMAC because the secret travels with every request — anyone who captures one request (a logged header, a misconfigured proxy) has your secret forever. It also says nothing about payload integrity. Fine for low-stakes internal webhooks; reach for HMAC when the events matter. ### [3. Basic auth](#_3-basic-auth) Username and password in the `Authorization` header. Same trade-offs as a shared token — the credential is sent on every request — but it's universally supported and trivial to configure on both ends. Only acceptable over HTTPS. Handy when a provider can't do HMAC but can do basic auth, or for quickly locking down an internal endpoint. ### [4. mTLS (mutual TLS)](#_4-mtls-mutual-tls) Both sides present X.509 certificates during the TLS handshake; your server only completes the connection if the client's cert is signed by a CA you trust. This authenticates the _connection_ itself, before any HTTP is exchanged — strong, but operationally heavy: you manage certificates, rotation and a CA. Reserved for high-security, often regulated, B2B integrations where both parties control the infrastructure. ### [5. IP allow-listing](#_5-ip-allow-listing) Only accept requests from the provider's published IP ranges. Cheap defense in depth, but **not sufficient alone**: it doesn't prove payload integrity, provider IP ranges change (breaking you silently), and shared cloud IPs can be reused by others. Best paired with HMAC. There's a flip side worth knowing: when _you_ are the one calling someone else's allow-listed endpoint, your requests need a **predictable source IP**. A forwarder with a [static outgoing IP](https://webhookrelay.com/blog/webhook-authentication/features/static-outgoing-ip/) gives you one stable address to hand to a partner's firewall, instead of a rotating pool you can't allow-list. ### [6. JWT](#_6-jwt) The provider signs a **JSON Web Token** (typically with HMAC or an RSA/ECDSA key) and sends it in the `Authorization` header. You verify the signature and the standard claims (`exp` for expiry, `iss` for issuer, `aud` for audience). JWTs carry their own expiry and metadata, which makes them nice for short-lived authorization and for verifying with a _public_ key (so your side never holds a shared secret). The cost is more moving parts than a plain HMAC body signature. ## [Quick comparison](#quick-comparison) | Method | Proves payload integrity? | Secret on the wire? | Setup cost | Good for | | --- | --- | --- | --- | --- | | **HMAC signature** | Yes | No | Low | The default — almost everything | | **Shared token** | No | Yes | Very low | Low-stakes / internal | | **Basic auth** | No | Yes | Very low | Legacy / quick lockdown | | **mTLS** | Connection-level | No | High | Regulated B2B | | **IP allow-list** | No | n/a | Low | Defense in depth only | | **JWT** | Yes (token) | Depends | Medium | Short-lived / public-key verify | ## [When to use which](#when-to-use-which) - **Building a new endpoint?** Start with **HMAC signature verification**. It's the best ratio of security to effort and what most providers already speak. - **Provider can't sign bodies?** Use **basic auth or a shared token over HTTPS**, and add **IP allow-listing** on top. - **Money, health, or compliance on the line?** Consider **mTLS** for connection-level guarantees, still with HMAC at the application layer. - **Need expiring, delegated access or public-key verification?** Reach for **JWT**. - **Always:** layer it. Signature _plus_ IP allow-list _plus_ a timestamp/replay check is meaningfully stronger than any single control. ## [How Webhook Relay fits in](#how-webhook-relay-fits-in) Webhook Relay sits in front of your services and can enforce authentication on its **endpoints** — supporting **HMAC**, **JWT** and **basic auth** so unauthenticated traffic is rejected before it ever reaches your code (see the [webhook auth docs](https://webhookrelay.com/blog/webhook-authentication/docs/webhooks/auth/hmac/) for the options). On the outbound side, the [static outgoing IP](https://webhookrelay.com/blog/webhook-authentication/features/static-outgoing-ip/) gives you a stable address to put on a partner's allow-list, and [transformations](https://webhookrelay.com/blog/webhook-authentication/features/transform-webhooks/) let you add or rewrite auth headers in flight when a destination expects a different scheme than the source provides. For the broader picture — replay protection, raw-body verification, HTTPS, and least-privilege handlers — see our [webhook security best practices](https://webhookrelay.com/blog/webhook-authentication/blog/webhook-security/). ## [The short version](#the-short-version) - **HMAC signatures** are the default: they prove authenticity _and_ integrity and never expose the secret. Verify the **raw body** with a **constant-time** compare. - **Tokens and basic auth** are simple but send the secret every time — fine for low-stakes endpoints over HTTPS. - **mTLS** and **JWT** cover stronger or more dynamic needs at higher operational cost. - **IP allow-listing** is great defense in depth but never a standalone control. - **Layer multiple methods**, and add a timestamp/replay check. Test your signature logic now with the [HMAC verifier](https://webhookrelay.com/blog/webhook-authentication/hmac-verification/), read [how to verify a webhook signature](https://webhookrelay.com/blog/webhook-authentication/blog/verify-webhook-signature/), or [create a free account](https://my.webhookrelay.com/register) to put authenticated endpoints in front of your services. --- --- title: Webhook Retries and Idempotency: A Practical Guide meta: "og: title": "Webhook Retries and Idempotency: A Practical Guide" description: Why webhook deliveries fail, how providers retry with exponential backoff, and how to build idempotent handlers that dedupe on event ID. url: https://webhookrelay.com/blog/webhook-retries-and-idempotency.md file: /blog/webhook-retries-and-idempotency.md --- ![Stripes](https://webhookrelay.com/blog/webhook-retries-and-idempotency/images/stripes.svg) # **Webhook Retries and Idempotency: A Practical Guide** Why webhook deliveries fail, how providers retry with exponential backoff, and how to build idempotent handlers that dedupe on event ID. Webhooks fail. Not often, but often enough that "fire it once and hope" is not a delivery strategy. A request times out, a deploy restarts your service for ten seconds, a load balancer hiccups — and the event is gone unless someone retries it. That's why every serious webhook provider retries, and why every serious receiver has to be **idempotent**. This guide covers the two halves of reliable webhook delivery: how **retries** work on the sending side, and how **idempotency** protects you on the receiving side. If you only learn one thing, make it this — **design your handler so processing the same event twice is harmless.** ## [Why deliveries fail](#why-deliveries-fail) A webhook is just an HTTP POST from a machine you don't control to a machine the sender can't see inside. Plenty can go wrong: - **Timeouts.** Your handler does slow work inline (charging a card, calling another API) and blows past the provider's timeout — often just a few seconds. - **Transient errors.** A `502`/`503` from your proxy during a deploy, a database that's briefly unreachable, a pod being rescheduled. - **Network drops.** The connection dies _after_ you processed the event but _before_ the provider got your `200`. The provider assumes failure. - **Bad responses.** Returning a non-`2xx` for any reason — including an uncaught exception — tells the provider to retry. The takeaway: failure is normal, and a chunk of "failures" are actually successes the sender never heard about. That's the root cause of duplicates. ## [How providers retry: exponential backoff](#how-providers-retry-exponential-backoff) When a delivery fails, providers don't hammer your endpoint — they back off. **Exponential backoff** roughly doubles the wait between attempts so a struggling endpoint gets room to recover: ``` attempt 1 → fail → wait ~10s attempt 2 → fail → wait ~30s attempt 3 → fail → wait ~2m attempt 4 → fail → wait ~10m attempt 5 → fail → wait ~1h ...up to hours or days, often with jitter ``` The exact schedule varies — Stripe retries for up to ~3 days, GitHub retries a handful of times, others differ — but the pattern is the same: increasing delays, capped attempts, then the provider gives up. Many add **jitter** (a small random offset) so a fleet of retries doesn't synchronize into a thundering herd. What "success" means to the sender is almost always the same: **a `2xx` status, returned quickly.** Anything else is a retry. This is why the cardinal rule of webhook handlers is _acknowledge fast, work later_ — return `200` as soon as you've durably stored the event, then do the heavy lifting in a background job. ## [At-least-once delivery (and why exactly-once is a myth)](#at-least-once-delivery-and-why-exactly-once-is-a-myth) Because the network can drop the acknowledgement, no provider can promise exactly-once delivery over plain HTTP. What they offer is **at-least-once**: the event is delivered one or more times, and it's your job to make repeats harmless. Treat "I might see this event again" as a guarantee, not an edge case. ## [Why duplicates happen — concretely](#why-duplicates-happen-concretely) You'll get the same event twice when: 1. Your handler succeeds but responds too slowly, so the provider's timeout fires and it retries. 2. The connection drops after processing but before the `2xx` reaches the sender. 3. You return a `500` from a non-critical bug _after_ the important side effect already ran. In every case the side effect (an order created, an email sent, a balance updated) happened once but the event arrives twice. Without protection, you double-charge, double-ship, or double-notify. ## [Making handlers idempotent](#making-handlers-idempotent) Idempotency means processing an event N times has the same effect as processing it once. The pattern is short: 1. **Find a stable unique ID** on the event. Most providers send one — Stripe's `id` (`evt_...`), GitHub's `X-GitHub-Delivery`, etc. Use the provider's ID, not a hash of the body, since payloads can vary. 2. **Record it before acting.** Insert the ID into a `processed_events` table with a **unique constraint**. 3. **Skip if seen.** If the insert hits the constraint, you've already handled this event — return `200` and stop. ``` -- one row per event, the unique index does the dedupe CREATE TABLE processed_events ( event_id TEXT PRIMARY KEY, created_at TIMESTAMPTZ DEFAULT now() ); ``` ``` def handle(event): try: db.execute( "INSERT INTO processed_events (event_id) VALUES (%s)", [event["id"]], ) except UniqueViolation: return 200, "already processed" # duplicate — no-op do_the_work(event) # safe: runs at most once per event id return 200, "ok" ``` The critical detail: **the dedupe check and the side effect must be atomic.** If you check-then-act in two steps, two concurrent retries can both pass the check. Wrap them in one transaction, or rely on the database's unique constraint to be the gatekeeper. ### [Idempotency keys for outbound calls](#idempotency-keys-for-outbound-calls) When _your_ handler calls another API (charging a card, creating an order), pass an **idempotency key** — a value derived from the webhook's event ID — so the downstream service also dedupes. Stripe, for example, accepts an `Idempotency-Key` header and will return the original result instead of charging twice. Now the whole chain is safe end to end. ## [Dead-letter patterns](#dead-letter-patterns) Retries don't last forever. After a provider exhausts its attempts (or your own forwarder does), the event is effectively lost unless you've captured it. A **dead-letter queue (DLQ)** is where you park deliveries that never succeeded so a human or a job can replay them later. Good practice: - Persist the raw payload the moment it arrives, before any processing. - Route permanently-failing events to a DLQ with the error and attempt count. - Build a one-click **replay** so you can re-send a fixed batch once the bug is patched. ## [How Webhook Relay helps](#how-webhook-relay-helps) A managed forwarder takes a lot of this off your plate. [Webhook Relay](https://webhookrelay.com/blog/webhook-retries-and-idempotency/webhooks/) **retries failed deliveries** with backoff, so a brief blip in your service doesn't drop the event — it's redelivered when you recover. Because it captures every request, you can **[replay](https://webhookrelay.com/blog/webhook-retries-and-idempotency/blog/how-to-test-webhooks/)** a payload against your handler as many times as you need (perfect for exercising your idempotency logic without re-triggering the source event). You can also [filter and route](https://webhookrelay.com/blog/webhook-retries-and-idempotency/features/forwarding-rules/) only the events you care about, and [transform](https://webhookrelay.com/blog/webhook-retries-and-idempotency/features/transform-webhooks/) them in flight — for example, normalizing the event ID into a header your handler dedupes on. You still own idempotency on your side — no forwarder can make a non-idempotent handler safe — but combining provider retries, a forwarder that retries, and a handler that dedupes gives you genuinely reliable delivery. ## [The short version](#the-short-version) - Deliveries fail for boring, frequent reasons; **retries with exponential backoff** are the cure. - Delivery is **at-least-once**, so **duplicates are guaranteed**, not rare. - Make handlers **idempotent**: dedupe on the provider's event ID, do it atomically, and pass **idempotency keys** to downstream APIs. - Capture raw payloads and keep a **dead-letter / replay** path for what slips through. Want to see this in action? [Inspect and replay real payloads in Webhook Bin](https://webhookrelay.com/blog/webhook-retries-and-idempotency/webhook-bin/), read [what a webhook is](https://webhookrelay.com/blog/webhook-retries-and-idempotency/blog/what-is-webhook/), or [create a free account](https://my.webhookrelay.com/register) to forward and retry webhooks against your own code. --- --- title: Security & Tech | WebhookRelay meta: "og: title": "Security & Tech" description: We will address the most common questions about the system, protocols involved, and security policies. url: https://webhookrelay.com/docs/security.md file: /docs/security.md --- ![Stripes](https://webhookrelay.com/docs/security/images/stripes.svg) Documentation **Fundamentals** # **Security & Tech** We will address the most common questions about the system, protocols involved, and security policies. ![SOC 2 Type II](https://webhookrelay.com/docs/security/images/soc2.svg) As a company that has achieved SOC 2 Type II certification, we understand the critical importance of information security in today's digital landscape. The increasing frequency and sophistication of cyber attacks highlight the necessity for businesses to prioritize security to safeguard their data and ensure the trust and confidence of their clients. By implementing industry-standard security measures and best practices, we demonstrate our unwavering commitment to the protection of sensitive information and the preservation of the integrity of our operations. We take pride in the rigorous security protocols we have in place and are dedicated to maintaining the highest standards of security excellence. ## [Background](#background) Webhook Relay is designed to increase network security by allowing internal services to receive webhooks and HTTP connections without exposing entire servers to the Internet. Each user gets one or more public URL endpoints (we call them **inputs** and they are grouped in **buckets**). When Webhook Relay agents connect to our servers, they have to authenticate using token key & secret pair and provide a list of **buckets** that they want to subscribe to. Once our servers receive a webhook, it's routed based on input ID to one or more agents: ![Webhook routing](https://webhookrelay.com/docs/security/images/docs/routing.png) In this document, we will address the most common questions about the system, protocols involved, and security policies. > If you have any other questions, feel free to send us an email at [](https://webhookrelay.com/docs/security//mailto:info@webhookrelay.com) [info@webhookrelay.com](https://webhookrelay.com/docs/security//mailto:info@webhookrelay.com). ### [Endpoints](#endpoints) All input endpoints are created based on UUID (universally unique identifiers) and can only belong to one bucket. Buckets can enable basic (username & password) or token-based authentication for all inputs. Both existing and non-existent input endpoints by default will return response status code 200. ### [Responses](#responses) By default, webhook forwarding is **unidirectional**. This means that responses from the destination will not be returned to the caller. This acts as a security layer, isolating your system and ensuring that the caller doesn't know what kind of system (if there is any) is receiving the webhooks. This is useful since most of the services are using response headers to identify themselves and libraries used to implement HTTP stack. Optionally, you can configure inputs to return static, dynamic (created by [Functions](https://webhookrelay.com/docs/security/docs/webhooks/functions/)) or wait for the destination to respond before returning HTTP response. ### [Connections](#connections) For internal destinations, a Webhook Relay agent is required. It's available as a Linux/Windows/macOS executable that can configure itself as an operating system background service or Docker container. You can view installation instructions [here](https://webhookrelay.com/docs/security/docs/installation/cli/). The agent opens a [GRPC](https://grpc.io/) connection to our public servers. Any webhooks, received by our servers will be matched to agents and streamed. Alternatively, a WebSocket connection can be used. All connections use [TLS](https://en.wikipedia.org/wiki/Transport_Layer_Security) (Transport Layer Security), therefore traffic is always encrypted in transit. For connections, [token authentication](https://my.webhookrelay.com/tokens) is required. ### [High Availability](#high-availability) Webhook Relay servers run on [Google Cloud](https://cloud.google.com/) Kubernetes platform to achieve high-availability. The agent uses health checks for connection monitoring and after reconnect (if your server reboots) it will resend missed webhooks from the last 10 minutes that weren't sent yet. It is recommended to run Webhook Relay agent in a container scheduler such as Kubernetes, OpenShift, Nomad, Swarm to ensure that it's always online. ### [Protocol](#protocol) Communication protocol for webhook forwarding is either [GRPC](https://grpc.io/) or WebSockets. You can view our [Socket Server documentation](https://webhookrelay.com/docs/security/docs/webhooks/websocket-server/) for message format. The protocol only allows HTTP requests, so your servers will never be exposed to SSH or other vulnerabilities even if your SaaS account was compromised. Agents also can't be switched between tunnel (bidirectional) and webhook forwarding modes via API, it has to be chosen during the installation. ### [Passwords, Tokens](#passwords-tokens) We use [bcrypt](https://en.wikipedia.org/wiki/Bcrypt) for password and token secret hashing, therefore if you lose your token secret, you will need to create a new one as they are not recoverable. ### [Data Storage](#data-storage) Webhook data is encrypted with AES-256 at rest. All buckets have also an option to become 'ephemeral' which means that body, headers, and query arguments are never stored in the database. Only the status code and time will be stored for audit and billing purposes. Alternatively, a self-hosted version of Webhook Relay is available if you want to keep data on your Own servers. Did this page help you? --- --- title: Hookdeck Alternative for Private Delivery | WebhookRelay meta: "og: title": "Hookdeck Alternative for Private Delivery" description: Compare Hookdeck and Webhook Relay. Both queue, retry and transform — but Webhook Relay also delivers to localhost and private networks, and tunnels. url: https://webhookrelay.com/blog/hookdeck-alternative.md file: /blog/hookdeck-alternative.md --- ![Stripes](https://webhookrelay.com/blog/hookdeck-alternative/images/stripes.svg) # **A Hookdeck Alternative for Delivering Webhooks to Private Infrastructure** Compare Hookdeck and Webhook Relay. Both queue, retry and transform — but Webhook Relay also delivers to localhost and private networks, and tunnels. ![A Hookdeck Alternative for Delivering Webhooks to Private Infrastructure](https://webhookrelay.com/blog/hookdeck-alternative/images/blog/heroes/alternative.jpg) [Hookdeck](https://hookdeck.com) is a polished webhook-infrastructure product: ingest, queue, retry, filter, transform and route webhooks, with a clean console and strong reliability. If you're comparing it with Webhook Relay, the products overlap a lot — the deciding factor is usually **where the webhook needs to end up**. Hookdeck delivers to public HTTP endpoints. Webhook Relay does that too, _and_ delivers to **localhost, private servers and Kubernetes** through a lightweight agent. ## [TL;DR](#tldr) - **Delivering to a public URL or SaaS?** Both are great. Choose on price and ergonomics. - **Delivering into private infrastructure** (on-prem API, internal service, K8s pod, a dev laptop in production-like flows)? Webhook Relay's agent does this natively. - **Want tunnels and scheduled/cron webhooks too?** Webhook Relay includes both. ## [Hookdeck vs Webhook Relay](#hookdeck-vs-webhook-relay) | | Hookdeck | Webhook Relay | | --- | --- | --- | | Ingest, queue, retry | Yes | Yes | | Filtering / routing | Yes | [Yes](https://webhookrelay.com/blog/hookdeck-alternative/features/forwarding-rules/) | | Transformations | Yes | [JS / Lua + AI](https://webhookrelay.com/blog/hookdeck-alternative/features/transform-webhooks/) | | Deliver to public endpoints | Yes | Yes | | Deliver to **localhost / private network** | Dev CLI only | Yes (relay agent) | | General-purpose tunnels | No | [Yes](https://webhookrelay.com/blog/hookdeck-alternative/tunnels/) | | Scheduled / cron webhooks | No | [Yes](https://webhookrelay.com/blog/hookdeck-alternative/cron/) | | Kubernetes operator | No | Yes | | Static outgoing IP | Add-on | [Yes](https://webhookrelay.com/blog/hookdeck-alternative/features/static-outgoing-ip/) | | Free webhook inspector | Console | [Webhook Bin](https://webhookrelay.com/blog/hookdeck-alternative/webhook-bin/) | | Starting paid price | ~$39/mo | $9.99/mo | _Competitor details reflect publicly documented plans as of 2026; verify current pricing on hookdeck.com._ ## [Where Hookdeck is strong](#where-hookdeck-is-strong) - A mature, well-designed console and developer experience. - Solid reliability and a deep set of guides. - A good fit when every destination is a **public HTTPS endpoint**. If that describes your setup and the price works, Hookdeck is a fine choice. ## [Where Webhook Relay is different](#where-webhook-relay-is-different) ### [Delivery into private networks](#delivery-into-private-networks) This is the core distinction. Webhook providers can only call public URLs — but your handler often lives somewhere private. The [relay agent](https://webhookrelay.com/blog/hookdeck-alternative/webhooks/) makes an outbound connection and tunnels webhooks to: ``` relay forward --bucket payments http://localhost:8080/stripe # or an internal host relay forward --bucket payments http://payments.internal:9000/hook ``` No public IP, no inbound firewall rules. Hookdeck's CLI covers local development, but Webhook Relay treats private delivery as a first-class production feature, including a [Kubernetes operator](https://webhookrelay.com/blog/hookdeck-alternative/features/webhook-kubernetes-integration/). ### [Tunnels and cron in the same product](#tunnels-and-cron-in-the-same-product) Beyond webhooks, Webhook Relay gives you general-purpose [localhost tunnels](https://webhookrelay.com/blog/hookdeck-alternative/tunnels/) and [scheduled (cron) webhooks](https://webhookrelay.com/blog/hookdeck-alternative/cron/) — useful adjacent jobs you'd otherwise wire up with separate tools. ### [Lower entry price](#lower-entry-price) For small and mid-size teams, $9.99–$79.99/month covers a lot of ground before you reach enterprise tiers. ## [When to pick which](#when-to-pick-which) - **Pick Hookdeck** if all destinations are public endpoints and you want its specific console/DX. - **Pick Webhook Relay** if you need to reach private infrastructure, want tunnels and cron in one place, or want a lower starting price. [Start free](https://my.webhookrelay.com/register) or [compare plans](https://webhookrelay.com/blog/hookdeck-alternative/pricing/). New to webhooks? [Inspect one in the browser](https://webhookrelay.com/blog/hookdeck-alternative/webhook-bin/) first. ![Stripes](https://webhookrelay.com/blog/hookdeck-alternative/images/stripes-dark.svg) ## **Start forwarding webhooks in minutes ** Connect a source, pick a destination, and Webhook Relay handles delivery, retries and transforms. Set up your first webhook in under five minutes. [Start for free ->](https://my.webhookrelay.com/register) Free plan · No credit card required · 7-day money-back guarantee on paid plans --- --- title: Svix Alternative for Private Webhook Delivery | WebhookRelay meta: "og: title": "Svix Alternative for Private Webhook Delivery" description: Compare Svix and Webhook Relay. Svix sends webhooks to your customers; Webhook Relay receives third-party webhooks and forwards them to private networks. url: https://webhookrelay.com/blog/svix-alternative.md file: /blog/svix-alternative.md --- ![Stripes](https://webhookrelay.com/blog/svix-alternative/images/stripes.svg) # **A Svix Alternative for Receiving and Forwarding Webhooks Into Private Infrastructure** Compare Svix and Webhook Relay. Svix sends webhooks to your customers; Webhook Relay receives third-party webhooks and forwards them to private networks. ![A Svix Alternative for Receiving and Forwarding Webhooks Into Private Infrastructure](https://webhookrelay.com/blog/svix-alternative/images/blog/heroes/alternative.jpg) [Svix](https://www.svix.com) is one of the best-known names in webhook infrastructure, and for good reason: the team authored the [Standard Webhooks](https://www.standardwebhooks.com) specification, ships a fully open-source MIT-licensed server, and powers outbound webhooks for companies like Clerk, Resend and Lob. If you're evaluating it against Webhook Relay, the most important thing to understand is that the two products solve **opposite directions** of the webhook problem. Svix helps _you_ send webhooks **to your own customers**. Webhook Relay helps you **receive** webhooks from third parties and forward them anywhere — including into private infrastructure that has no public IP. ## [TL;DR](#tldr) - **Building a platform that sends webhooks to your customers?** Svix is purpose-built for that, with an embeddable consumer portal, FIFO ordering and strong compliance. - **Receiving webhooks from providers (Stripe, GitHub, Shopify, CI) and routing them somewhere?** That's Webhook Relay's job. - **Need delivery to localhost, an internal service or a Kubernetes pod?** Webhook Relay's relay agent does this natively; Svix doesn't. - **Want tunnels and scheduled/cron webhooks too?** Webhook Relay includes both. ## [Svix vs Webhook Relay](#svix-vs-webhook-relay) | | Svix | Webhook Relay | | --- | --- | --- | | Primary direction | Send to your customers (outbound) | Receive & forward (inbound) | | Standard Webhooks spec author | Yes | No | | Self-hostable open-source server | Yes (MIT) | No (hosted) | | Compliance (SOC2 / HIPAA / PCI) | Yes | SOC2-oriented; verify current scope | | FIFO ordering | Yes | Ordered delivery per bucket | | Transformations | Yes | [JS / Lua + AI](https://webhookrelay.com/blog/svix-alternative/features/transform-webhooks/) | | Deliver to public endpoints | Yes (your customers' URLs) | [Yes](https://webhookrelay.com/blog/svix-alternative/features/webhook-multiple-destinations/) | | Deliver to **localhost / private network** | No | Yes ([relay agent](https://webhookrelay.com/blog/svix-alternative/webhooks/)) | | General-purpose tunnels | No | [Yes](https://webhookrelay.com/blog/svix-alternative/tunnels/) | | Scheduled / cron webhooks | No | [Yes](https://webhookrelay.com/blog/svix-alternative/cron/) | | Kubernetes operator | No | [Yes](https://webhookrelay.com/blog/svix-alternative/features/webhook-kubernetes-integration/) | | Static outgoing IP | n/a | [Yes](https://webhookrelay.com/blog/svix-alternative/features/static-outgoing-ip/) | | Cloud Service Connections (S3/SQS/SNS, Pub/Sub) | No | Yes | | Free webhook inspector | No | [Webhook Bin](https://webhookrelay.com/blog/svix-alternative/webhook-bin/) | | Starting paid price | ~$490/mo (as of 2026) | $9.99/mo | _Competitor details reflect publicly documented plans as of 2026; verify current pricing and features on svix.com._ ## [Where Svix is strong](#where-svix-is-strong) - **Sending webhooks to your customers at scale.** If you run a SaaS platform and need to deliver event notifications to thousands of customer endpoints — with retries, automatic disabling of dead endpoints, and customer notifications — Svix is built for exactly this. - **An embeddable consumer portal.** Your customers get a UI to manage their own endpoints, inspect deliveries and replay events, without you building it. - **The Standard Webhooks lineage.** Svix authored the spec that OpenAI, Anthropic, Google and others have adopted, so its signing and delivery conventions are well thought through. - **Open source and self-hostable.** The MIT-licensed server is the same codebase as the hosted SaaS, which is attractive if you need on-prem or want to avoid lock-in. - **Compliance.** SOC2, HIPAA and PCI coverage make it a comfortable fit for regulated platforms. If you're a _producer_ of webhooks and your customers are the consumers, Svix is likely the better fit and we'd happily point you there. ## [Where Webhook Relay is different](#where-webhook-relay-is-different) ### [It's built for the receiving end](#its-built-for-the-receiving-end) Most teams comparing these tools aren't trying to _send_ webhooks to their own customers — they're trying to **consume** webhooks that someone else (Stripe, GitHub, Shopify, a CI system) sends _them_, and get those events to the right place. That "right place" is frequently not a public URL. ### [Delivery into private networks](#delivery-into-private-networks) This is the core distinction. A webhook provider can only call a public URL — but your handler often lives somewhere private. The relay agent makes an **outbound** connection and forwards webhooks to wherever you point it: ``` relay forward --bucket payments http://localhost:8080/stripe # or an internal host with no public IP relay forward --bucket payments http://payments.internal:9000/hook ``` No inbound firewall rules, no public IP required, including a first-class [Kubernetes operator](https://webhookrelay.com/blog/svix-alternative/features/webhook-kubernetes-integration/) for delivering into your cluster. Svix delivers to your customers' public endpoints; it isn't designed to run a persistent agent into _your_ private infrastructure. ### [Fan-out, transformations and cron in one product](#fan-out-transformations-and-cron-in-one-product) Webhook Relay can [fan a single webhook out to multiple destinations](https://webhookrelay.com/blog/svix-alternative/features/webhook-multiple-destinations/), reshape payloads in flight with [JavaScript or Lua transformations](https://webhookrelay.com/blog/svix-alternative/features/transform-webhooks/), give you a [static outgoing IP](https://webhookrelay.com/blog/svix-alternative/features/static-outgoing-ip/) for allow-listing, and run [scheduled (cron) webhooks](https://webhookrelay.com/blog/svix-alternative/cron/) — none of which are Svix's focus. It also offers general-purpose [localhost tunnels](https://webhookrelay.com/blog/svix-alternative/tunnels/) and [Service Connections](https://webhookrelay.com/blog/svix-alternative/) to AWS S3/SQS/SNS and GCP Pub/Sub/GCS, so events can land directly in cloud infrastructure. ### [A free request inspector and a lower entry price](#a-free-request-inspector-and-a-lower-entry-price) Svix doesn't offer a consumer-facing free request bin; Webhook Relay does — the free [Webhook Bin](https://webhookrelay.com/blog/svix-alternative/webhook-bin/) lets anyone inspect incoming requests in the browser, no account required. And for small and mid-size teams, $9.99–$79.99/month covers a lot of ground before you reach enterprise tiers, versus a Professional plan reported around $490/month (as of 2026, verify current pricing). ## [When to pick which](#when-to-pick-which) - **Pick Svix** if you're a platform that needs to **send** webhooks to your own customers at scale, want an embeddable consumer portal, need a self-hostable open-source server, or need its compliance posture. - **Pick Webhook Relay** if you need to **receive** third-party webhooks and forward them — especially into localhost, internal services or Kubernetes — or you want tunnels, fan-out, transformations and cron in one place at a lower starting price. [Start free](https://my.webhookrelay.com/register) or [compare plans](https://webhookrelay.com/blog/svix-alternative/pricing/). New to webhooks? [Inspect one in the browser](https://webhookrelay.com/blog/svix-alternative/webhook-bin/) first. ![Stripes](https://webhookrelay.com/blog/svix-alternative/images/stripes-dark.svg) ## **Start forwarding webhooks in minutes ** Connect a source, pick a destination, and Webhook Relay handles delivery, retries and transforms. Set up your first webhook in under five minutes. [Start for free ->](https://my.webhookrelay.com/register) Free plan · No credit card required · 7-day money-back guarantee on paid plans --- --- title: Pipedream Alternative for Webhook Routing | WebhookRelay meta: "og: title": "Pipedream Alternative for Webhook Routing" description: Compare Pipedream and Webhook Relay. Pipedream is no-code automation; Webhook Relay is webhook infrastructure that reaches localhost and private networks. url: https://webhookrelay.com/blog/pipedream-alternative.md file: /blog/pipedream-alternative.md --- ![Stripes](https://webhookrelay.com/blog/pipedream-alternative/images/stripes.svg) # **A Pipedream Alternative for Webhook Routing and Private Delivery** Compare Pipedream and Webhook Relay. Pipedream is no-code automation; Webhook Relay is webhook infrastructure that reaches localhost and private networks. ![A Pipedream Alternative for Webhook Routing and Private Delivery](https://webhookrelay.com/blog/pipedream-alternative/images/blog/heroes/alternative.jpg) [Pipedream](https://pipedream.com) is a powerful workflow-automation platform: connect hundreds of SaaS apps, trigger on events, and run Node or Python code in between. It also owns RequestBin, which many developers first encountered as a quick way to inspect webhooks. If you're comparing it with Webhook Relay, the key question is whether you want a **general-purpose automation builder** or **dedicated webhook infrastructure**. Pipedream is great when you're orchestrating many apps with code. Webhook Relay is purpose-built for one job — moving webhooks reliably from where they're sent to wherever they need to go, **including private destinations Pipedream can't reach**. ## [TL;DR](#tldr) - **Building multi-step automations across many SaaS apps?** Pipedream's integration directory and code-on-event model are excellent. - **Just need to receive, route, transform and deliver webhooks reliably?** Webhook Relay is dedicated to that and is simpler to reason about. - **Need delivery to localhost, an internal service or Kubernetes?** Webhook Relay's relay agent does this natively; Pipedream doesn't. - **Worried about unpredictable cost at high webhook volume?** Webhook Relay uses flat pricing, not credits. ## [Pipedream vs Webhook Relay](#pipedream-vs-webhook-relay) | | Pipedream | Webhook Relay | | --- | --- | --- | | Product type | Automation / integration platform | Dedicated webhook infrastructure | | Huge SaaS integration directory | Yes | No (webhook-focused) | | Code-on-event (Node / Python) | Yes | [JS / Lua transforms](https://webhookrelay.com/blog/pipedream-alternative/features/transform-webhooks/) | | Receive & forward webhooks | Yes | Yes | | Deliver to public endpoints | Yes | [Yes](https://webhookrelay.com/blog/pipedream-alternative/features/webhook-multiple-destinations/) | | Deliver to **localhost / private network** | No | Yes ([relay agent](https://webhookrelay.com/blog/pipedream-alternative/webhooks/)) | | General-purpose tunnels | No | [Yes](https://webhookrelay.com/blog/pipedream-alternative/tunnels/) | | Scheduled / cron webhooks | Yes (workflow schedules) | [Yes](https://webhookrelay.com/blog/pipedream-alternative/cron/) | | Kubernetes operator | No | [Yes](https://webhookrelay.com/blog/pipedream-alternative/features/webhook-kubernetes-integration/) | | Static outgoing IP | No | [Yes](https://webhookrelay.com/blog/pipedream-alternative/features/static-outgoing-ip/) | | Cloud Service Connections (S3/SQS/SNS, Pub/Sub) | Via integrations | Yes (native) | | Free webhook inspector | RequestBin (account required) | [Webhook Bin](https://webhookrelay.com/blog/pipedream-alternative/webhook-bin/) (no account) | | Pricing model | Credit-based | Flat | | Starting paid price | ~$29/mo (as of 2026) | $9.99/mo | _Competitor details reflect publicly documented plans as of 2026; verify current pricing and features on pipedream.com._ ## [Where Pipedream is strong](#where-pipedream-is-strong) - **A massive integration directory.** If your job is to wire together dozens of SaaS apps — read a row from Airtable, call an API, post to Slack, write to a database — Pipedream's pre-built connectors save real time. - **Code-on-event flexibility.** You can drop into Node or Python at any step, which makes complex, branching logic straightforward. - **No-code/low-code builder.** Non-engineers and engineers alike can assemble workflows visually. - **RequestBin heritage.** It's a familiar on-ramp for inspecting incoming requests (though it now lives behind a Pipedream account). If you want an automation platform that connects many tools, Pipedream is a strong choice and we'd point you there. ## [Where Webhook Relay is different](#where-webhook-relay-is-different) ### [It's dedicated webhook infrastructure, not an automation platform](#its-dedicated-webhook-infrastructure-not-an-automation-platform) Pipedream is a general-purpose orchestration tool that _happens_ to accept webhooks. Webhook Relay does one thing: receive webhooks and get them where they need to go — reliably, with retries, ordering and observability — without you assembling a workflow. For pure webhook routing, that's less to configure and easier to reason about. ### [Delivery into private networks](#delivery-into-private-networks) This is the biggest gap. Pipedream runs in its cloud and delivers to public URLs; it can't push events into infrastructure with no public IP. Webhook Relay's relay agent makes an **outbound** connection and forwards webhooks anywhere you point it: ``` relay forward --bucket payments http://localhost:8080/stripe # or an internal host with no public IP relay forward --bucket payments http://payments.internal:9000/hook ``` No inbound firewall rules, no public IP, and a first-class [Kubernetes operator](https://webhookrelay.com/blog/pipedream-alternative/features/webhook-kubernetes-integration/) for delivering straight into your cluster. This is a production capability, not just a local-dev convenience. ### [Fan-out, transforms, static IP and tunnels](#fan-out-transforms-static-ip-and-tunnels) Webhook Relay can [fan a single webhook out to multiple destinations](https://webhookrelay.com/blog/pipedream-alternative/features/webhook-multiple-destinations/), reshape payloads in flight with [JavaScript or Lua transformations](https://webhookrelay.com/blog/pipedream-alternative/features/transform-webhooks/), give you a [static outgoing IP](https://webhookrelay.com/blog/pipedream-alternative/features/static-outgoing-ip/) to allow-list with strict firewalls, run [scheduled (cron) webhooks](https://webhookrelay.com/blog/pipedream-alternative/cron/), and open general-purpose [localhost tunnels](https://webhookrelay.com/blog/pipedream-alternative/tunnels/). It also has native Service Connections to AWS S3/SQS/SNS and GCP Pub/Sub/GCS for landing events directly in cloud infrastructure. ### [Predictable, flat pricing](#predictable-flat-pricing) Pipedream's credit-based model (where roughly one credit equals 30 seconds of compute, as of 2026) is flexible but can be hard to forecast for high or spiky webhook volume. Webhook Relay uses flat pricing — $9.99/month (Team) and $79.99/month (Business) — so a busy day of webhooks doesn't surprise you on the bill. ### [A free inspector with no account](#a-free-inspector-with-no-account) Pipedream's RequestBin now requires a Pipedream account. Webhook Relay's free [Webhook Bin](https://webhookrelay.com/blog/pipedream-alternative/webhook-bin/) lets anyone inspect incoming requests in the browser with no sign-up needed. ## [When to pick which](#when-to-pick-which) - **Pick Pipedream** if you want a no-code/low-code automation builder that connects many SaaS apps, run custom Node/Python on events, or orchestrate multi-step workflows beyond webhook routing. - **Pick Webhook Relay** if you mainly need to receive, route, transform and deliver webhooks — especially into localhost, internal services or Kubernetes — want tunnels, fan-out, a static outgoing IP and cron in one place, or want flat, predictable pricing. [Start free](https://my.webhookrelay.com/register) or [compare plans](https://webhookrelay.com/blog/pipedream-alternative/pricing/). New to webhooks? [Inspect one in the browser](https://webhookrelay.com/blog/pipedream-alternative/webhook-bin/) first. ![Stripes](https://webhookrelay.com/blog/pipedream-alternative/images/stripes-dark.svg) ## **Start forwarding webhooks in minutes ** Connect a source, pick a destination, and Webhook Relay handles delivery, retries and transforms. Set up your first webhook in under five minutes. [Start for free ->](https://my.webhookrelay.com/register) Free plan · No credit card required · 7-day money-back guarantee on paid plans --- --- title: Zapier Webhooks Alternative for Developers | WebhookRelay meta: "og: title": "Zapier Webhooks Alternative for Developers" description: Compare Webhooks by Zapier and Webhook Relay. Zapier is no-code automation; Webhook Relay is developer webhook infrastructure with private delivery. url: https://webhookrelay.com/blog/zapier-webhooks-alternative.md file: /blog/zapier-webhooks-alternative.md --- ![Stripes](https://webhookrelay.com/blog/zapier-webhooks-alternative/images/stripes.svg) # **A Zapier Webhooks Alternative for Developer-Grade Webhook Delivery** Compare Webhooks by Zapier and Webhook Relay. Zapier is no-code automation; Webhook Relay is developer webhook infrastructure with private delivery. ![A Zapier Webhooks Alternative for Developer-Grade Webhook Delivery](https://webhookrelay.com/blog/zapier-webhooks-alternative/images/blog/heroes/alternative.jpg) [Webhooks by Zapier](https://zapier.com) is a familiar starting point for a lot of teams, because Zapier is already in the building. But if you're comparing it against Webhook Relay, the important thing to understand is that they're really two different categories of product. Zapier is a **no-code automation platform**. "Webhooks by Zapier" is a single trigger/action among thousands of app integrations, useful for gluing SaaS apps together. Webhook Relay is **developer webhook infrastructure**: its whole job is to receive webhooks and forward them reliably to wherever they need to go, including destinations Zapier can't reach. ## [TL;DR](#tldr) - **Connecting many SaaS apps with no code, where webhooks are incidental?** Zapier is purpose-built for that, with an enormous app directory. - **Receiving webhooks from providers (Stripe, GitHub, Shopify, CI) and routing them as infrastructure?** That's Webhook Relay's job. - **Need delivery to localhost, an internal service or a Kubernetes pod?** Webhook Relay's relay agent does this natively; Zapier delivers to public endpoints only. - **Worried about cost at volume?** Zapier bills per task; Webhook Relay's paid plans start at $9.99/month with a free tier. ## [Webhooks by Zapier vs Webhook Relay](#webhooks-by-zapier-vs-webhook-relay) | | Webhooks by Zapier | Webhook Relay | | --- | --- | --- | | Primary purpose | No-code SaaS automation | Webhook delivery infrastructure | | App directory / integrations | Thousands of apps | Not an app directory | | Pricing model | Per task/operation | Flat monthly, not per task | | Webhooks on free plan | No (paid plans) | [Yes](https://webhookrelay.com/blog/zapier-webhooks-alternative/pricing/) | | Deliver to public endpoints | Yes | [Yes](https://webhookrelay.com/blog/zapier-webhooks-alternative/features/webhook-multiple-destinations/) | | Deliver to **localhost / private network** | No | Yes ([relay agent](https://webhookrelay.com/blog/zapier-webhooks-alternative/webhooks/)) | | Fan-out to multiple destinations | Limited (multi-step Zaps) | [Yes](https://webhookrelay.com/blog/zapier-webhooks-alternative/features/webhook-multiple-destinations/) | | Transformations | Code by Zapier steps | [JS / Lua + AI](https://webhookrelay.com/blog/zapier-webhooks-alternative/features/transform-webhooks/) | | Scheduled / cron webhooks | Via Schedule app | [Yes, native](https://webhookrelay.com/blog/zapier-webhooks-alternative/cron/) | | Static outgoing IP | No | [Yes](https://webhookrelay.com/blog/zapier-webhooks-alternative/features/static-outgoing-ip/) | | Kubernetes operator | No | [Yes](https://webhookrelay.com/blog/zapier-webhooks-alternative/features/webhook-kubernetes-integration/) | | General-purpose tunnels | No | [Yes](https://webhookrelay.com/blog/zapier-webhooks-alternative/tunnels/) | | Free webhook inspector | No | [Webhook Bin](https://webhookrelay.com/blog/zapier-webhooks-alternative/webhook-bin/) | | Starting paid price | ~$19.99/mo (Starter, annual, as of 2026) | $9.99/mo | _Competitor details reflect publicly documented plans as of 2026; verify current pricing and features on zapier.com._ ## [Where Zapier is strong](#where-zapier-is-strong) - **An enormous app directory.** Zapier connects thousands of SaaS tools, and for many of them you'll never touch a raw webhook. If your goal is "when a row is added in Google Sheets, post to Slack and create a Trello card," Zapier is excellent and Webhook Relay is the wrong tool. - **No-code, for non-developers.** Zaps are built in a visual editor, so marketing, ops and sales teams can automate workflows without writing code. That accessibility is the whole point of the platform. - **Brand and ecosystem.** Zapier is a known quantity with templates, a large community and a long catalog of pre-built integrations. If webhooks are _incidental_ to what you're doing — a small piece of a broader no-code automation across many apps — Zapier is likely the better fit, and we'd happily point you there. ## [Where Webhook Relay is different](#where-webhook-relay-is-different) ### [It's webhook infrastructure, not an automation platform](#its-webhook-infrastructure-not-an-automation-platform) Most teams comparing these tools aren't trying to wire ten SaaS apps together. They're trying to **reliably move webhooks** — take an event a provider sends them and get it to the right place, at scale, without it getting lost. That's a plumbing problem, and Webhook Relay is plumbing built specifically for webhooks. ### [Delivery into private networks](#delivery-into-private-networks) This is the core distinction. A webhook provider — or Zapier — can only call a public URL. But your handler often lives somewhere private. The relay agent makes an **outbound** connection and forwards webhooks to wherever you point it: ``` relay forward --bucket payments http://localhost:8080/stripe # or an internal host with no public IP relay forward --bucket payments http://payments.internal:9000/hook ``` No inbound firewall rules, no public IP required, including a first-class [Kubernetes operator](https://webhookrelay.com/blog/zapier-webhooks-alternative/features/webhook-kubernetes-integration/) for delivering into your cluster. As of 2026, Webhooks by Zapier can only deliver to public endpoints and explicitly cannot reach a server behind a firewall — verify current Zapier behavior before relying on it. ### [Pricing that doesn't punish volume](#pricing-that-doesnt-punish-volume) Zapier bills by tasks/operations, so a high-traffic webhook flow can get expensive quickly, and webhooks are only unlocked on paid plans (Starter is around $19.99/month billed annually as of 2026 — verify current pricing). Webhook Relay charges a flat monthly price rather than per event, so [Team at $9.99/month and Business at $79.99/month](https://webhookrelay.com/blog/zapier-webhooks-alternative/pricing/) cover a lot of throughput before you scale up, and there's a free plan to start. ### [Fan-out, transformations and cron built for webhooks](#fan-out-transformations-and-cron-built-for-webhooks) Webhook Relay can [fan a single webhook out to multiple destinations](https://webhookrelay.com/blog/zapier-webhooks-alternative/features/webhook-multiple-destinations/), reshape payloads in flight with [JavaScript or Lua transformations](https://webhookrelay.com/blog/zapier-webhooks-alternative/features/transform-webhooks/), give you a [static outgoing IP](https://webhookrelay.com/blog/zapier-webhooks-alternative/features/static-outgoing-ip/) for allow-listing on the receiving side, and run [scheduled (cron) webhooks](https://webhookrelay.com/blog/zapier-webhooks-alternative/cron/) — all as native webhook primitives rather than steps inside a Zap. It also offers general-purpose [localhost tunnels](https://webhookrelay.com/blog/zapier-webhooks-alternative/tunnels/) for exposing any local service. ### [A free request inspector](#a-free-request-inspector) Zapier doesn't offer a consumer-facing free request bin; Webhook Relay does — the free [Webhook Bin](https://webhookrelay.com/blog/zapier-webhooks-alternative/webhook-bin/) lets anyone inspect incoming requests in the browser, no account required, which is handy when you're debugging exactly what a provider is sending. ## [When to pick which](#when-to-pick-which) - **Pick Zapier** if you want to connect many SaaS apps with no code, your team isn't writing software, and webhooks are an incidental part of a broader automation. - **Pick Webhook Relay** if you need dependable webhook delivery as infrastructure — especially into localhost, internal services or Kubernetes — predictable flat pricing, or webhook-native fan-out, transformations and cron. [Start free](https://my.webhookrelay.com/register) or [compare plans](https://webhookrelay.com/blog/zapier-webhooks-alternative/pricing/). New to webhooks? [Inspect one in the browser](https://webhookrelay.com/blog/zapier-webhooks-alternative/webhook-bin/) first. ![Stripes](https://webhookrelay.com/blog/zapier-webhooks-alternative/images/stripes-dark.svg) ## **Start forwarding webhooks in minutes ** Connect a source, pick a destination, and Webhook Relay handles delivery, retries and transforms. Set up your first webhook in under five minutes. [Start for free ->](https://my.webhookrelay.com/register) Free plan · No credit card required · 7-day money-back guarantee on paid plans --- --- title: Cloudflare Tunnel Alternative for Webhooks | WebhookRelay meta: "og: title": "Cloudflare Tunnel Alternative for Webhooks" description: Compare Cloudflare Tunnel and Webhook Relay for exposing localhost and forwarding webhooks — a stable URL with no domain to manage, plus retries. url: https://webhookrelay.com/blog/cloudflare-tunnel-alternative.md file: /blog/cloudflare-tunnel-alternative.md --- ![Stripes](https://webhookrelay.com/blog/cloudflare-tunnel-alternative/images/stripes.svg) # **A Cloudflare Tunnel Alternative for Webhooks and Localhost Tunnels (2026)** Compare Cloudflare Tunnel and Webhook Relay for exposing localhost and forwarding webhooks — a stable URL with no domain to manage, plus retries. ![A Cloudflare Tunnel Alternative for Webhooks and Localhost Tunnels (2026)](https://webhookrelay.com/blog/cloudflare-tunnel-alternative/images/blog/heroes/alternative.jpg) If you searched for a **Cloudflare Tunnel alternative**, you've probably run into one of two friction points: the production "named" tunnels want you to own a domain _and_ manage its DNS in Cloudflare, while the no-account [`trycloudflare.com`](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/do-more-with-tunnels/trycloudflare/) quick tunnels hand you a random URL that changes and is meant only for testing. [Cloudflare Tunnel](https://www.cloudflare.com/) is a genuinely great piece of infrastructure for exposing a service you own. But if your actual job is _receiving and forwarding webhooks_, a raw tunnel leaves the webhook-specific work — inspection, transforms, retries, fan-out — for you to build yourself. Webhook Relay comes at the same problem from the webhook side: a stable public URL with no domain to manage, plus the tooling to inspect, transform and route what arrives. ## [TL;DR](#tldr) - **Want a tunnel without owning a domain or moving DNS?** Webhook Relay gives you a stable public endpoint out of the box on every plan, including free. - **Just exposing a local web app or API?** Both work. Webhook Relay [tunnels](https://webhookrelay.com/blog/cloudflare-tunnel-alternative/tunnels/) skip the `cloudflared` config and domain setup. - **Testing provider webhooks (Stripe, GitHub, Shopify)?** Use the free [Webhook Bin](https://webhookrelay.com/blog/cloudflare-tunnel-alternative/webhook-bin/) to inspect them in your browser, then forward to localhost with the agent. - **Cloudflare Tunnel is still the better pick** when you already live on Cloudflare, own the domain, and want a permanently free tunnel for a service you control. ## [Cloudflare Tunnel vs Webhook Relay at a glance](#cloudflare-tunnel-vs-webhook-relay-at-a-glance) | | Cloudflare Tunnel | Webhook Relay | | --- | --- | --- | | Stable public URL | Named tunnel (needs a domain) | Yes, every plan | | No-domain quick URL | `trycloudflare.com` (random, ephemeral) | Stable endpoint, no domain needed | | Requires owning a domain in Cloudflare | Yes (for named tunnels) | No | | Outbound-only agent (no firewall ports) | Yes (`cloudflared`) | Yes ([relay agent](https://webhookrelay.com/blog/cloudflare-tunnel-alternative/tunnels/)) | | Forward to localhost / private network | Yes | Yes | | Inspect requests in a browser | No | Yes ([Webhook Bin](https://webhookrelay.com/blog/cloudflare-tunnel-alternative/webhook-bin/)) | | Webhook-aware (understands the payload) | No (raw proxy) | Yes | | Transform payloads (JS/Lua) | No | [Yes](https://webhookrelay.com/blog/cloudflare-tunnel-alternative/features/transform-webhooks/) | | Fan-out to multiple destinations | No | [Yes](https://webhookrelay.com/blog/cloudflare-tunnel-alternative/features/webhook-multiple-destinations/) | | Retries on failure | No | Yes | | Scheduled / cron webhooks | No | [Yes](https://webhookrelay.com/blog/cloudflare-tunnel-alternative/cron/) | | Starting paid price | Free (tunnel itself) | $9.99/mo (free plan available) | _Competitor details reflect publicly documented behavior as of 2026 and can change — verify Cloudflare's current pricing, setup requirements and quick-tunnel limits before deciding._ ## [Where Cloudflare Tunnel shines](#where-cloudflare-tunnel-shines) Let's be fair — Cloudflare Tunnel is excellent at what it's built for: - **A permanently free tunnel** to a service you own, with no per-tunnel cost. As of 2026 the tunnel itself carries no usage fee; verify current terms on Cloudflare's pricing page. - **Tight integration with the Cloudflare edge** and Zero Trust — Access policies, WAF, DDoS protection and caching all sit in front of your origin. - **Outbound-only `cloudflared`** that connects from your origin, so there are no inbound firewall ports to open. If you already run on Cloudflare, own a domain there, and just need to reach a private origin, Cloudflare Tunnel is hard to beat. ## [Where Webhook Relay wins for webhooks](#where-webhook-relay-wins-for-webhooks) ### [1. A stable URL with no domain to manage](#_1-a-stable-url-with-no-domain-to-manage) Cloudflare's production tunnels expect you to own a domain _and_ run its DNS in Cloudflare. The no-domain `trycloudflare.com` option exists, but those URLs are random, ephemeral, and explicitly for testing — historically with limits like ~200 concurrent requests and no Server-Sent Events (verify current limits). With Webhook Relay, your endpoint is **stable on every plan** without owning a domain — set it once in Stripe or GitHub and forget it. ### [2. Forward to localhost _and_ private networks — without the daemon setup](#_2-forward-to-localhost-and-private-networks-without-the-daemon-setup) Both tools route traffic into private networks via an outbound agent. The difference is friction: Webhook Relay's agent is a single command, no `cloudflared` config files or DNS records to wire up: ``` # Install the agent, then forward your public endpoint to a local port relay forward --bucket my-app http://localhost:8080/webhook ``` The agent connects **outbound**, so there are no firewall ports to open — same security posture as `cloudflared`, less setup. ### [3. Understand the webhook, don't just proxy it](#_3-understand-the-webhook-dont-just-proxy-it) This is the core distinction. Cloudflare Tunnel moves bytes; it has no concept of a webhook. Because Webhook Relay sits in the path _as a webhook tool_, you can [transform payloads](https://webhookrelay.com/blog/cloudflare-tunnel-alternative/features/transform-webhooks/) with JavaScript or Lua (turn a raw GitHub event into a Slack message), [fan-out to multiple destinations](https://webhookrelay.com/blog/cloudflare-tunnel-alternative/features/webhook-multiple-destinations/), filter noisy events, add authentication, and **retry on failure**. A raw tunnel can't do any of that. ### [4. Inspect requests in the browser — no install](#_4-inspect-requests-in-the-browser-no-install) Open [Webhook Bin](https://webhookrelay.com/blog/cloudflare-tunnel-alternative/webhook-bin/), get an instant URL, and watch requests arrive in real time with full headers and bodies — and even configure [custom responses](https://webhookrelay.com/blog/cloudflare-tunnel-alternative/webhook-bin/). Cloudflare Tunnel has no equivalent browser inspector; you'd reach for separate tooling. ## [How to switch from Cloudflare Tunnel in 2 minutes](#how-to-switch-from-cloudflare-tunnel-in-2-minutes) 1. **Inspect first (no install):** open [Webhook Bin](https://webhookrelay.com/blog/cloudflare-tunnel-alternative/webhook-bin/), copy the URL, and point your provider at it. 2. **Forward to localhost:** [create a free account](https://my.webhookrelay.com/register), install the agent, and run `relay forward` — no domain, no DNS records, no `cloudflared` config. 3. **Keep the URL forever:** your endpoint is stable, so you never re-configure the provider. For a general-purpose reverse proxy to any local port, see the [tunnels documentation](https://webhookrelay.com/blog/cloudflare-tunnel-alternative/tunnels/). ## [When to pick which](#when-to-pick-which) - **Pick Cloudflare Tunnel** when you already run on Cloudflare, own the domain, want a permanently free tunnel to a service you control, and don't need webhook features. - **Pick Webhook Relay** when the work is webhooks: a stable URL with no domain to manage, forwarding to private infrastructure without `cloudflared` setup, and inspecting, transforming, retrying or fanning-out events. Ready to skip the domain-and-daemon setup? [Start forwarding for free](https://my.webhookrelay.com/register) or [test a webhook now](https://webhookrelay.com/blog/cloudflare-tunnel-alternative/webhook-bin/). ![Stripes](https://webhookrelay.com/blog/cloudflare-tunnel-alternative/images/stripes-dark.svg) ## **Start forwarding webhooks in minutes ** Connect a source, pick a destination, and Webhook Relay handles delivery, retries and transforms. Set up your first webhook in under five minutes. [Start for free ->](https://my.webhookrelay.com/register) Free plan · No credit card required · 7-day money-back guarantee on paid plans --- --- title: Agent Skills | WebhookRelay meta: "og: title": "Agent Skills" description: Install the open-source Webhook Relay Agent Skills so Claude and other agents can forward, transform, debug, tunnel and schedule webhooks with the CLI. url: https://webhookrelay.com/docs/skills.md file: /docs/skills.md --- ![Stripes](https://webhookrelay.com/docs/skills/images/stripes.svg) Documentation **Fundamentals** # **Agent Skills** Install the open-source Webhook Relay Agent Skills so Claude and other agents can forward, transform, debug, tunnel and schedule webhooks with the CLI. ## [Overview](#overview) [Agent Skills](https://agentskills.io/specification) are small, self-contained instruction packs that teach Claude (and other skill-aware agents) how to perform a specific task. Webhook Relay publishes an open-source set of Skills that show agents how to drive the platform with the `relay` CLI and API — forwarding webhooks, transforming them, debugging incoming requests, exposing services over tunnels, scheduling recurring webhooks, and receiving, parsing and transforming inbound email. The Skills are maintained on GitHub: - [github.com/webhookrelay/skills](https://github.com/webhookrelay/skills) Use that repository to browse the source, open issues, or contribute new skills. ## [Quick start](#quick-start) Install every skill with the [skills.sh CLI](https://webhookrelay.com/docs/skills/docs/skills-cli/): ``` npx skills add webhookrelay/skills ``` That drops the skills into your agent's skills directory. From then on you can ask your agent in plain language — it picks the right skill and runs the `relay` commands for you: | Ask your agent | Skill it uses | | --- | --- | | _"Give me a webhook URL to test my Stripe integration"_ | `webhook-debug` | | _"Forward my GitHub webhooks to localhost:3000"_ | `webhook-forwarding-internal` | | _"Relay this webhook to Slack without running anything locally"_ | `webhook-forwarding-public` | | _"Reshape this payload into a Slack message before delivering it"_ | `webhook-transformations` | | _"Expose my local dev server on a public HTTPS URL"_ | `relay-tunnels` | | _"Send a health-check webhook every 5 minutes"_ | `recurring-webhooks` | | _"Give me a throwaway email address and read what it receives"_ | `temporary-email` | | _"Turn incoming emails into JSON and POST them to my API"_ | `transform-email-to-api-call` | See [all the ways to install](#installation) — single skill, Claude Code plugin, ClawHub, or manual — below. ## [Skills vs. MCP](#skills-vs-mcp) Skills and the [MCP server](https://webhookrelay.com/docs/skills/docs/mcp/) solve related but different problems, and they work well together: - **Skills** are instructions. They teach an agent _how_ to use the `relay` CLI/API correctly — the right commands, payload shapes, and workflows. They run wherever your agent has a shell. - **[MCP](https://webhookrelay.com/docs/skills/docs/mcp/)** is a live connection. It gives an agent typed tools to manage buckets, inspect webhook logs, and work with transform functions directly against your account. Install the Skills for guided, command-line workflows; connect MCP when you want the agent to call Webhook Relay tools directly. ## [Available skills](#available-skills) | Skill | What it does | | --- | --- | | `webhook-debug` | Capture and inspect webhooks with a free, no-signup bin at [bin.webhookrelay.com](https://bin.webhookrelay.com) — a public URL that records any HTTP request so you can see the exact method, headers, query and body a provider sends, stream requests live, mock the response, and verify HMAC signatures. No CLI or account required. | | `webhook-forwarding-internal` | Receive webhooks from providers (Stripe, GitHub, Shopify, CI, etc.) and forward them to a destination with no public IP — `localhost`, a private LAN host, or a Kubernetes service. The `relay` agent performs the final hop, so no inbound firewall ports are needed. Best for local development. | | `webhook-forwarding-public` | Forward webhooks server-side from a public Webhook Relay endpoint to another internet-reachable URL — no local agent required. Relay between cloud services, put a stable URL in front of an API, fan one webhook out to many destinations, or transform payloads in transit. | | `webhook-transformations` | Write, test, and attach JavaScript (or Lua) functions that modify webhooks in flight — reshape the JSON body, rename fields, add/remove headers, change the method or path, set the response, drop requests conditionally, or call other HTTP APIs. | | `relay-tunnels` | Expose a local or internal HTTP/TCP service on a stable public hostname (an ngrok-style reverse proxy) without opening firewall ports — share a dev server, demo a local app, expose an internal API, or tunnel TCP (SSH, databases). | | `recurring-webhooks` | Schedule cron-driven webhooks that fire automatically — send a recurring HTTP request (method, body, headers) to one or more destinations on an interval or at specific times. Use for health checks, heartbeats, scheduled reports, or triggering a function on a timer. | | `temporary-email` | Create a disposable / temporary inbound email address from the terminal and read the mail it receives as JSON — no mailbox, SMTP server, or per-address signup. Grab a throwaway inbox for a signup / OTP / confirmation link, receive one-off mail in a script, or watch an address for incoming messages. | | `email-parsing-api` | Turn inbound email into structured JSON — create an inbound address and every message is parsed into a stable payload (from, to/cc, subject, text, html, headers, spf/dkim/dmarc, attachments), delivered to your endpoint or pulled from the events API. An "email → JSON" API with no SMTP/IMAP server. | | `transform-email-to-api-call` | Turn inbound emails into calls to your own API — a bucket with an email address, a public output pointing at your API, and a JavaScript function that reshapes the parsed email into the exact request your endpoint expects. Email-driven automation, e.g. create a ticket/lead/record from an email. | ## [Installation](#installation) The fastest way to install is with the [skills.sh CLI](https://webhookrelay.com/docs/skills/docs/skills-cli/): ``` npx skills add webhookrelay/skills ``` Add `-g` to install globally for all your agents: ``` npx skills add webhookrelay/skills -g ``` ### [Install a single skill](#install-a-single-skill) Pass `--skill ` to install just one: ``` npx skills add webhookrelay/skills --skill webhook-debug ``` ### [Claude Code plugin marketplace](#claude-code-plugin-marketplace) The repository doubles as a Claude Code plugin marketplace. Add it, then install the plugin: ``` /plugin marketplace add webhookrelay/skills /plugin install webhookrelay-skills ``` ### [ClawHub](#clawhub) The skills are also published on [ClawHub](https://clawhub.ai), the open registry for AI agent skills. Install the bundled [Webhook Relay skill](https://clawhub.ai/rusenask/webhook-relay) with the `openclaw` CLI: ``` openclaw skills install webhook-relay ``` See [Webhook Relay on ClawHub](https://webhookrelay.com/docs/skills/docs/clawhub/) for what the registry does and how the skill fits. ### [Manual install](#manual-install) You can also copy any `skills//` folder from the [repository](https://github.com/webhookrelay/skills) into your agent's skills directory. ## [Prerequisites](#prerequisites) The `webhook-debug` skill works against the public bin API with nothing but `curl` — no account or key needed. Every other skill uses the `relay` CLI: 1. Install it — see [CLI installation](https://webhookrelay.com/docs/skills/docs/installation/cli/). 2. Log in: `relay login` (or set the `RELAY_KEY` / `RELAY_SECRET` environment variables). 3. Verify: `relay bucket ls`. ## [Concepts at a glance](#concepts-at-a-glance) - **Bucket** — groups inputs and outputs. - **Input** — a public HTTPS endpoint that receives webhooks. - **Output** — a destination requests are relayed to; `internal` (delivered by a running agent, e.g. localhost) or `public` (delivered server-side). - **Function** — server-side JavaScript/Lua that transforms requests/responses. - **Tunnel** — a public hostname that proxies inbound HTTP/TCP to a local or internal service. - **Cron** — a scheduled, recurring webhook. See [Getting Started](https://webhookrelay.com/docs/skills/docs/) for the full model. ## [Issues and contributions](#issues-and-contributions) The Skills are open source. To report a problem, request a new skill, or read the code, use the GitHub repository: - [github.com/webhookrelay/skills](https://github.com/webhookrelay/skills) — browse the code and [open an issue](https://github.com/webhookrelay/skills/issues). ## [Links](#links) - [Webhook Relay Skills on GitHub](https://github.com/webhookrelay/skills) - [skills.sh CLI](https://webhookrelay.com/docs/skills/docs/skills-cli/) — what it is and how to install with it - [Webhook Relay on ClawHub](https://webhookrelay.com/docs/skills/docs/clawhub/) - [MCP server](https://webhookrelay.com/docs/skills/docs/mcp/) - [Functions reference](https://webhookrelay.com/docs/skills/docs/webhooks/functions/) - [CLI installation](https://webhookrelay.com/docs/skills/docs/installation/cli/) - [Dashboard](https://my.webhookrelay.com) - LLM-friendly docs index — append `.md` to any docs URL for plain markdown, or see [llms.txt](https://webhookrelay.com/llms.txt) Did this page help you? --- --- title: webhook.site Alternative for Webhook Testing | WebhookRelay meta: "og: title": "webhook.site Alternative for Webhook Testing" description: A webhook.site alternative that does more than inspect: an instant webhook URL, real-time requests, then forwarding to localhost or a private server. url: https://webhookrelay.com/blog/webhook-site-alternative.md file: /blog/webhook-site-alternative.md --- ![Stripes](https://webhookrelay.com/blog/webhook-site-alternative/images/stripes.svg) # **A webhook.site Alternative for Testing and Forwarding Webhooks** A webhook.site alternative that does more than inspect: an instant webhook URL, real-time requests, then forwarding to localhost or a private server. ![A webhook.site Alternative for Testing and Forwarding Webhooks](https://webhookrelay.com/blog/webhook-site-alternative/images/blog/heroes/alternative.jpg) [webhook.site](https://webhook.site) is the tool most developers reach for to see what a webhook actually contains. It's genuinely useful — paste the URL, watch requests land. But the moment you want to _do_ something with those requests — replay them to your app, forward them to localhost, or keep a permanent URL — you hit its edges. Webhook Relay's free [Webhook Bin](https://webhookrelay.com/blog/webhook-site-alternative/webhook-bin/) covers the same instant-inspection use case, and then keeps going: the same platform forwards webhooks to your own machine or private network. ## [TL;DR](#tldr) - **Just need to see a payload?** Both work. Open [Webhook Bin](https://webhookrelay.com/blog/webhook-site-alternative/webhook-bin/) and you're inspecting in seconds, no signup. - **Need those webhooks to reach your app on localhost?** Webhook Bin → the relay agent does it; webhook.site stops at inspection. - **Want a permanent URL you configure once?** Create a free Webhook Relay account and keep your endpoint forever. ## [webhook.site vs Webhook Bin](#webhooksite-vs-webhook-bin) | | webhook.site | Webhook Relay (Webhook Bin) | | --- | --- | --- | | Instant URL, no signup | Yes | Yes | | Real-time request inspector | Yes | Yes | | Custom response (status, body) | Yes | Yes | | Forward to **localhost** | No | Yes (relay agent) | | Forward to a **private server / Kubernetes** | No | Yes | | Transform payloads (JS/Lua) | Limited | [Yes](https://webhookrelay.com/blog/webhook-site-alternative/features/transform-webhooks/) | | Fan-out to multiple destinations | No | [Yes](https://webhookrelay.com/blog/webhook-site-alternative/features/webhook-multiple-destinations/) | | Permanent endpoint | Paid | Yes (free account) | | Path from _testing_ to _production_ | No | Yes | ## [The real difference: testing is step one](#the-real-difference-testing-is-step-one) webhook.site is a great **inspector**. Webhook Relay is an inspector **plus** a delivery platform, so the URL you test with can become the URL you ship with. A typical flow: 1. **Inspect (no install).** Open [Webhook Bin](https://webhookrelay.com/blog/webhook-site-alternative/webhook-bin/), copy the URL, point Stripe/GitHub/Shopify at it, and watch the requests arrive with full headers and body. 2. **Forward to your app.** [Create a free account](https://my.webhookrelay.com/register), install the agent, and deliver those same webhooks to `localhost`: ``` relay forward --bucket my-app http://localhost:8080/webhook ``` 1. **Go to production.** Keep the endpoint, add [retries](https://webhookrelay.com/blog/webhook-site-alternative/webhooks/), [transformations](https://webhookrelay.com/blog/webhook-site-alternative/features/transform-webhooks/), or [multiple destinations](https://webhookrelay.com/blog/webhook-site-alternative/features/webhook-multiple-destinations/) — without changing the URL your provider calls. ## [Where webhook.site is still handy](#where-webhooksite-is-still-handy) - Throwaway, zero-commitment inspection when you don't care about forwarding. - A shareable URL to ask "what is this service sending?" For those, either tool is fine. The reason to choose Webhook Relay is that **you rarely just want to look** — you want the webhook to end up in your code. ## [Try it](#try-it) [Open Webhook Bin](https://webhookrelay.com/blog/webhook-site-alternative/webhook-bin/) to inspect a request right now, or [create a free account](https://my.webhookrelay.com/register) to forward webhooks to localhost and beyond. See the [webhooks documentation](https://webhookrelay.com/blog/webhook-site-alternative/docs/webhooks/internal/localhost/) for the full localhost setup. ![Stripes](https://webhookrelay.com/blog/webhook-site-alternative/images/stripes-dark.svg) ## **Start forwarding webhooks in minutes ** Connect a source, pick a destination, and Webhook Relay handles delivery, retries and transforms. Set up your first webhook in under five minutes. [Start for free ->](https://my.webhookrelay.com/register) Free plan · No credit card required · 7-day money-back guarantee on paid plans --- --- title: MCP Server | WebhookRelay meta: "og: title": "MCP Server" description: Use the Webhook Relay MCP server to manage buckets, inputs, outputs, webhook logs, transform functions and cloud connections from AI agents. url: https://webhookrelay.com/docs/mcp.md file: /docs/mcp.md --- ![Stripes](https://webhookrelay.com/docs/mcp/images/stripes.svg) Documentation **Fundamentals** # **MCP Server** Use the Webhook Relay MCP server to manage buckets, inputs, outputs, webhook logs, transform functions and cloud connections from AI agents. ## [Overview](#overview) Webhook Relay provides an MCP (Model Context Protocol) server that lets AI agents manage your webhook infrastructure programmatically. You can configure and access MCP from: - [https://my.webhookrelay.com/mcp](https://my.webhookrelay.com/mcp) > Prefer guiding your agent through the `relay` CLI instead of (or alongside) MCP? See [Agent Skills](https://webhookrelay.com/docs/mcp/docs/skills/). The MCP API endpoint is: - `https://my.webhookrelay.com/v1/mcp` ## [Authentication](#authentication) Authenticate requests with a Bearer token in the `Authorization` header: ``` Authorization: Bearer ``` ## [Configure MCP in Webhook Relay](#configure-mcp-in-webhook-relay) Open the MCP page in your Webhook Relay account and use the provided configuration values in your MCP client or agent: ![Webhook Relay MCP configuration](https://webhookrelay.com/docs/mcp/images/docs/mcp/mcp_config.png) ## [Connect Claude.ai](#connect-claudeai) You can add Webhook Relay to Claude.ai as a **custom connector** using just the MCP endpoint URL — there is no API key to copy and paste, authentication happens in your browser when you connect. > Adding custom connectors requires a Claude **Pro, Max, Team, or Enterprise** plan. 1. In Claude.ai, open **Settings → Connectors** (on Team/Enterprise, an admin adds it under **Settings → Connectors → Organization connectors**). 2. Click **Add custom connector**. 3. Fill in the form and click **Add**: - **Name**: `webhookrelay` - **Remote MCP server URL**:``` https://my.webhookrelay.com/v1/mcp ``` 4. Find the new **webhookrelay** connector in the list and click **Connect**. A Webhook Relay sign-in / authorization window opens — log in and approve access to link the connector to your account (this is the OAuth step; no token needs to be entered manually). 5. In a chat, enable the **webhookrelay** connector from the tools/connectors menu, then try a prompt like: - "List all my webhook buckets" - "Create a new bucket that forwards to [https://example.com](https://example.com)" - "Which buckets had failed deliveries in the last 24 hours?" ![Claude.ai connection](https://webhookrelay.com/docs/mcp/images/docs/mcp/claude.png) ## [Use MCP from an Agent](#use-mcp-from-an-agent) Once configured, your agent can call MCP tools to create and manage buckets, inspect webhook logs, and work with transform functions: ![Webhook Relay MCP usage from an agent](https://webhookrelay.com/docs/mcp/images/docs/mcp/agent_use.png) ## [Resource](#resource) The MCP server exposes the following resource: | URI | Description | | --- | --- | | `webhookrelay://docs/functions/javascript-api` | Complete API reference for writing JavaScript transform functions. Covers the request object (`r`), config (`cfg`), HTTP client, crypto, time, BigQuery, and Mailgun modules. | ## [Tools](#tools) ### [Bucket Management](#bucket-management) | Tool | Description | Params | | --- | --- | --- | | `list_buckets` | List all webhook buckets for the account, including their inputs (public endpoints) and outputs (forwarding destinations). | None | | `create_bucket` | Create a new bucket with a default public endpoint. Optionally create an output in the same call by providing a destination URL. Returns the public webhook URL. Attach transform functions at creation time via `input_function_id` / `output_function_id`. | **`name`**, `description`, `destination`, `internal`, `input_function_id`, `output_function_id` | | `update_bucket` | Update a bucket's name or description. | **`id`**, `name`, `description` | | `delete_bucket` | Delete a bucket and all its inputs/outputs (destructive). | **`id`** | ### [Inputs (public endpoints)](#inputs-public-endpoints) `create_bucket` already creates one default input. Use these tools when a bucket needs more than one endpoint, or to fine-tune endpoint behaviour (custom response, custom domain, path prefix, TLS policy). | Tool | Description | Params | | --- | --- | --- | | `get_input` | Get an input's full configuration: response settings, custom domain, path prefix and TLS settings. Read-only. | **`input_id`** | | `create_input` | Create an additional public HTTPS endpoint in a bucket. All inputs in a bucket forward to the same outputs. | **`bucket_id`**, **`name`**, `description`, `function_id`, `response_status_code`, `response_body`, `response_headers`, `response_from_output`, `custom_domain`, `path_prefix`, `strip_path_prefix`, `tls_version`, `legacy_tls` | | `update_input` | Update an input. Only provided fields change; pass an empty string to clear a text field (e.g. `function_id: ""` detaches the function). | **`input_id`**, `name`, `description`, `function_id`, `response_status_code`, `response_body`, `response_headers`, `response_from_output`, `custom_domain`, `path_prefix`, `strip_path_prefix`, `tls_version`, `legacy_tls` | | `delete_input` | Delete an input. Its endpoint stops accepting webhooks immediately; logs are unaffected (destructive). | **`input_id`** | ### [Outputs (forwarding destinations)](#outputs-forwarding-destinations) | Tool | Description | Params | | --- | --- | --- | | `get_output` | Get an output's full configuration: override headers, retry/timeout settings, filtering rules and durable retry config. Read-only. | **`output_id`** | | `create_output` | Create a forwarding destination in a bucket. Every webhook received by the bucket's inputs is forwarded to each enabled output whose rules match. Use `internal=true` for private-network destinations reached via a relay agent. For cloud services (SQS, PubSub, …) use `create_service_connection_output` instead. | **`bucket_id`**, **`name`**, **`destination`**, `description`, `internal`, `function_id`, `headers`, `lock_path`, `disabled`, `retries`, `timeout_seconds`, `tls_verification`, `rules`, `durability` | | `update_output` | Update an output. Only provided fields change; pass an empty string to clear a text field. Filtering rules are managed separately via `set_output_rules` / `clear_output_rules`. | **`output_id`**, `name`, `description`, `destination`, `internal`, `function_id`, `headers`, `lock_path`, `disabled`, `retries`, `timeout_seconds`, `tls_verification`, `durability` | | `delete_output` | Delete an output. Webhooks are no longer forwarded to its destination (destructive). | **`output_id`** | | `set_output_rules` | Set filtering rules so an output only forwards webhooks for which the rule tree evaluates to true. See [Output filtering rules](#output-filtering-rules) below for the rule-tree format. | **`output_id`**, **`rules`** | | `clear_output_rules` | Remove all filtering rules so the output forwards every webhook again. | **`output_id`** | ### [Webhook Logs & Delivery Health](#webhook-logs-delivery-health) | Tool | Description | Params | | --- | --- | --- | | `list_webhook_logs` | List webhook logs for a bucket in summary format (status code, status, method, timestamp, duration). Filter by delivery status (e.g. `status=failed`), output and a `from`/`to` time window. Read-only. | **`bucket_id`**, `output_id`, `status`, `from`, `to`, `limit` (default 20), `offset`, `cursor` | | `get_webhook_log` | Get full details of a single log including request/response headers and body. Read-only. | **`id`**, **`bucket_id`** | | `get_logs_stats` | Get delivery statistics (received / success / failures). By default returns one per-day series for the account (or one bucket via `bucket_id`). Pass `group_by=bucket` for a per-bucket leaderboard ranked by failures — the fastest way to find _which_ buckets are failing in one call. Read-only. | `bucket_id`, `group_by` (`bucket`), `from`, `to`, `limit` (default 10, max 100), `offset` | | `retry_webhook` | Retry (resend) a webhook log through its bucket's delivery pipeline, or to a single output via `output_id`. `process_policy` controls whether rules and functions re-run (`skip` / `force`). | **`id`**, `bucket_id`, `output_id`, `process_policy` | > **Note:** `status` accepts `sent`, `failed`, `stalled`, `received` or `rejected`. `from` / `to` accept `YYYY-MM-DD` or RFC3339. `list_webhook_logs` and `get_webhook_log` are always bucket-scoped and **require `bucket_id`**. ### [Transform Functions](#transform-functions) | Tool | Description | Params | | --- | --- | --- | | `list_functions` | List all transform functions in the account. Read-only. | None | | `get_function` | Get a function's full details including source code. Read-only. | **`id`** | | `create_function` | Create a new transform function. Supported drivers: `lua`, `js`. | **`name`**, **`driver`**, **`code`** | | `update_function` | Update an existing function's name, driver, or code. | **`id`**, `name`, `driver`, `code` | | `execute` | Execute a transform function with a synthetic request payload. Use to test functions before attaching them. | **`function_id`**, `method`, `path`, `raw_query`, `headers`, `request_body` | ### [Function Attachments](#function-attachments) | Tool | Description | Params | | --- | --- | --- | | `attach_function` | Attach a transform function to an input or output so it processes webhooks in transit. | **`resource_type`** (`input`/`output`), **`resource_id`**, **`function_id`** | | `detach_function` | Detach a transform function from an input or output. | **`resource_type`** (`input`/`output`), **`resource_id`** | ### [Service Connections](#service-connections) | Tool | Description | Params | | --- | --- | --- | | `list_service_connections` | List all service connections (GCP, AWS, Azure) for the account. Read-only. | None | | `get_service_connection` | Get a service connection's details (secrets redacted). Read-only. | **`id`** | | `create_service_connection` | Create a connection to a cloud provider. GCP: `gcp_project_id` + `gcp_service_account_key`. AWS: `aws_access_key_id` + `aws_secret_access_key`. Azure: `azure_client_id` + `azure_client_secret` + `azure_tenant_id`. | **`name`**, **`service_type`** (`gcp`/`aws`/`azure`), provider credentials | | `update_service_connection` | Update a connection's name or credentials. Only non-empty fields are updated. | **`id`**, `name`, provider credentials | | `delete_service_connection` | Delete a service connection (destructive). | **`id`** | ### [Service Connection Inputs](#service-connection-inputs) | Tool | Description | Params | | --- | --- | --- | | `list_service_connection_inputs` | List cloud event subscription inputs for a bucket. Read-only. | **`bucket_id`** | | `create_service_connection_input` | Subscribe to cloud events and forward them into a bucket. Per `input_type`: `gcp_pubsub` → `gcp_pubsub_subscription_name`; `gcp_gcs` → `gcp_gcs_bucket_name`; `aws_s3` → `aws_s3_bucket_name` + `aws_s3_region`; `aws_sqs` → `aws_sqs_queue_url`; `aws_sns` → `aws_sns_topic_arn`. | **`bucket_id`**, **`service_connection_id`**, **`input_type`**, `name`, `function_id`, type-specific fields | | `delete_service_connection_input` | Delete a service connection input from a bucket (destructive). | **`bucket_id`**, **`input_id`** | ### [Service Connection Outputs](#service-connection-outputs) | Tool | Description | Params | | --- | --- | --- | | `list_service_connection_outputs` | List cloud service outputs for a bucket. Read-only. | **`bucket_id`** | | `create_service_connection_output` | Forward webhooks from a bucket to cloud services. Per `output_type`: `gcp_pubsub` → `gcp_pubsub_topic_name`; `gcp_gcs` → `gcp_gcs_bucket_name`; `aws_s3` → `aws_s3_bucket_name` + `aws_s3_region`; `aws_sqs` → `aws_sqs_queue_url`; `aws_sns` → `aws_sns_topic_arn`. | **`bucket_id`**, **`service_connection_id`**, **`output_type`**, `name`, `function_id`, type-specific fields | | `delete_service_connection_output` | Delete a service connection output from a bucket (destructive). | **`bucket_id`**, **`output_id`** | ## [Investigating delivery failures](#investigating-delivery-failures) The delivery-health tools are designed to localise failures fast, without looping a stats call per bucket: 1. **Find which buckets are failing** — call `get_logs_stats` with `group_by=bucket` for an account-wide leaderboard ranked by failure count (paged via `limit`/`offset`). 2. **Drill into a bucket's trend** — call `get_logs_stats` with `bucket_id` for its per-day received/success/failure series. 3. **Find the broken webhooks** — call `list_webhook_logs` with `bucket_id` and `status=failed` (optionally `from`/`to`/`output_id`). 4. **Inspect one** — call `get_webhook_log` with the log `id` and its `bucket_id` for full request/response detail. 5. **Re-deliver** — call `retry_webhook` with the log `id` (optionally a single `output_id`, and `process_policy` `skip`/`force`). ## [Output filtering rules](#output-filtering-rules) `set_output_rules` takes a rule tree with exactly one top-level key: - `match` — a single condition - `and` / `or` — arrays of nested rule trees - `not` — negates a nested tree A `match` condition has a `type` and operands. Common types: `value` (exact match, uses `value`), `contains` / `does-not-contain` (uses `substring`), `regex` (uses `regex`), `payload-hash-sha1` / `payload-hash-sha256` (HMAC signature verification, uses `secret`), and `ip-whitelist` (uses `ip-range` CIDR). The `parameter.source` is one of `payload` (JSON body field, dot notation for nesting), `header`, `query`, `path`, `string`, `entire-payload`, `entire-query` or `entire-headers`. Example — only forward GitHub `push` events on the `main` branch: ``` { "and": [ { "match": { "type": "value", "value": "refs/heads/main", "parameter": { "source": "payload", "name": "ref" } } }, { "match": { "type": "value", "value": "push", "parameter": { "source": "header", "name": "X-GitHub-Event" } } } ] } ``` ## [Key Concepts](#key-concepts) - **Bucket**: A logical container grouping one or more inputs with one or more outputs. - **Input**: A public HTTPS endpoint (URL) that receives webhooks from third parties. A bucket can have several inputs (each with its own URL, custom domain, response and TLS settings); they all feed the same outputs. - **Output**: A destination URL where received webhooks are forwarded. Set `internal=true` for private-network/localhost destinations (requires a connected relay agent). Outputs support override headers, retries/timeouts, filtering rules and durable long-period retries. - **Function**: A Lua or JavaScript snippet that transforms webhooks in transit. Functions are attached to inputs (runs before routing) or outputs (runs before forwarding). - **Service Connection**: Credentials for a cloud provider (GCP, AWS, Azure) that enable cloud event subscriptions (inputs) and cloud service forwarding (outputs). ## [Important Behavior for JavaScript Functions](#important-behavior-for-javascript-functions) Before creating or updating JavaScript transform functions, agents must first read: - `webhookrelay://docs/functions/javascript-api` JavaScript transforms run in a custom runtime. They must mutate the global `r` object directly, and standard Node.js/browser APIs are not available. Did this page help you? --- --- title: Beeceptor Alternative for Webhooks (2026) | WebhookRelay meta: "og: title": "Beeceptor Alternative for Webhooks (2026)" description: Compare Beeceptor and Webhook Relay: a free webhook bin with custom responses, plus forwarding to localhost and private servers, retries and fan-out. url: https://webhookrelay.com/blog/beeceptor-alternative.md file: /blog/beeceptor-alternative.md --- ![Stripes](https://webhookrelay.com/blog/beeceptor-alternative/images/stripes.svg) # **A Beeceptor Alternative for Inspecting and Forwarding Webhooks (2026)** Compare Beeceptor and Webhook Relay: a free webhook bin with custom responses, plus forwarding to localhost and private servers, retries and fan-out. ![A Beeceptor Alternative for Inspecting and Forwarding Webhooks (2026)](https://webhookrelay.com/blog/beeceptor-alternative/images/blog/heroes/alternative.jpg) If you searched for a **Beeceptor alternative**, you were probably testing a webhook — and either hit Beeceptor's tight free limits or realized that inspecting the request was only half the job. You also needed to get that webhook to your code running on **localhost** or a **private server**. [Beeceptor](https://beeceptor.com) is a polished mock-API server and request inspector. It's great for faking an endpoint and watching what arrives. But it's an _inspection and mocking_ tool, not a _delivery_ platform: it can't forward a captured webhook into your own infrastructure. Webhook Relay covers both sides — inspect a webhook in the browser for free, then actually forward, transform, retry and fan it out to wherever your handler lives. ## [TL;DR](#tldr) - **Just want to inspect a request or mock an API?** Beeceptor (or Webhook Relay's free [Webhook Bin](https://webhookrelay.com/blog/beeceptor-alternative/webhook-bin/)) both work — the Webhook Bin adds custom responses and no signup. - **Need that webhook to reach localhost or a private server?** Webhook Relay forwards into private networks; Beeceptor doesn't. - **Hitting Beeceptor's free request cap?** Webhook Relay has a free plan and forwarding-grade paid plans from $9.99/month. - **Beeceptor is still the better pick** when you specifically want a mock API server or just to inspect requests with no forwarding. ## [Beeceptor vs Webhook Relay at a glance](#beeceptor-vs-webhook-relay-at-a-glance) | | Beeceptor | Webhook Relay | | --- | --- | --- | | Inspect incoming requests in browser | Yes | Yes ([Webhook Bin](https://webhookrelay.com/blog/beeceptor-alternative/webhook-bin/)) | | Custom mock/response | Yes (mock server) | Yes (Webhook Bin custom responses) | | Free tier limits | Tight (~50 req/endpoint/day) | Free plan available | | Forward to localhost / private network | No | Yes ([relay agent](https://webhookrelay.com/blog/beeceptor-alternative/tunnels/)) | | Forward to public endpoints | No | Yes | | Transform payloads (JS/Lua) | No | [Yes](https://webhookrelay.com/blog/beeceptor-alternative/features/transform-webhooks/) | | Fan-out to multiple destinations | No | [Yes](https://webhookrelay.com/blog/beeceptor-alternative/features/webhook-multiple-destinations/) | | Retries on failure | No | Yes | | General-purpose tunnels | No | [Yes](https://webhookrelay.com/blog/beeceptor-alternative/tunnels/) | | Scheduled / cron webhooks | No | [Yes](https://webhookrelay.com/blog/beeceptor-alternative/cron/) | | Starting paid price | ~$10–25/mo | $9.99/mo (free plan available) | _Competitor details reflect publicly documented plans as of 2026 and can change — verify Beeceptor's current free limits and pricing on beeceptor.com before deciding._ ## [Where Beeceptor shines](#where-beeceptor-shines) Let's be fair — Beeceptor is very good at what it's built for: - **Mock API servers.** Stand up a fake endpoint with custom rules and responses to develop against before a real backend exists. - **Quick request inspection.** Point a provider at a Beeceptor endpoint and watch calls land, with no account needed to start. - **A clean, friendly UI** for QA and front-end work. If your goal is to _mock_ an API or just _look at_ requests — and you never need to deliver them anywhere — Beeceptor is a fine choice. ## [Where Webhook Relay wins for webhooks](#where-webhook-relay-wins-for-webhooks) ### [1. It forwards, not just inspects](#_1-it-forwards-not-just-inspects) This is the core distinction. Beeceptor captures a webhook so you can read it; Webhook Relay captures it and **delivers it to your code**. Run the [relay agent](https://webhookrelay.com/blog/beeceptor-alternative/tunnels/) and incoming webhooks tunnel straight to your laptop or an internal service: ``` # Install the agent, then forward your public endpoint to a local port relay forward --bucket my-app http://localhost:8080/webhook ``` The agent connects **outbound**, so there's no public IP and no firewall ports to open — webhooks reach `localhost`, a private LAN host, or a Kubernetes pod. ### [2. A free inspector with custom responses](#_2-a-free-inspector-with-custom-responses) You don't lose the inspection experience. Open [Webhook Bin](https://webhookrelay.com/blog/beeceptor-alternative/webhook-bin/), get an instant URL, and watch requests arrive in real time with full headers and bodies — and configure [custom responses](https://webhookrelay.com/blog/beeceptor-alternative/webhook-bin/) for the caller, no signup to start. It's the part of Beeceptor you reached for, kept free. ### [3. Do something to the webhook in flight](#_3-do-something-to-the-webhook-in-flight) Because Webhook Relay sits in the delivery path, you can [transform payloads](https://webhookrelay.com/blog/beeceptor-alternative/features/transform-webhooks/) with JavaScript or Lua (turn a raw Stripe event into a Slack message), [fan-out to multiple destinations](https://webhookrelay.com/blog/beeceptor-alternative/features/webhook-multiple-destinations/), filter noisy events, add authentication, and **retry on failure**. A mock/inspection tool stops at "here's what arrived." ### [4. Room to grow past the free cap](#_4-room-to-grow-past-the-free-cap) Beeceptor's free tier is tight — as of 2026 roughly 50 requests per endpoint per day (verify current limits). Webhook Relay has a free plan for getting started, and paid plans from $9.99/month that unlock forwarding, transforms, retries and fan-out — the things you need once testing turns into a real integration. ## [How to switch from Beeceptor in 2 minutes](#how-to-switch-from-beeceptor-in-2-minutes) 1. **Inspect first (no install):** open [Webhook Bin](https://webhookrelay.com/blog/beeceptor-alternative/webhook-bin/), copy the URL, and point your provider at it. 2. **Forward to localhost:** [create a free account](https://my.webhookrelay.com/register), install the agent, and run `relay forward`. 3. **Keep the URL forever:** your endpoint is stable, so you never re-configure the provider. ## [When to pick which](#when-to-pick-which) - **Pick Beeceptor** when you specifically want a mock API server, or just to inspect requests with no forwarding. - **Pick Webhook Relay** when the work is webhooks end to end: inspect them for free, then forward to private infrastructure, transform, retry and fan-out — without hitting a tight request cap. Ready to go past inspection? [Start forwarding for free](https://my.webhookrelay.com/register) or [test a webhook now](https://webhookrelay.com/blog/beeceptor-alternative/webhook-bin/). ![Stripes](https://webhookrelay.com/blog/beeceptor-alternative/images/stripes-dark.svg) ## **Start forwarding webhooks in minutes ** Connect a source, pick a destination, and Webhook Relay handles delivery, retries and transforms. Set up your first webhook in under five minutes. [Start for free ->](https://my.webhookrelay.com/register) Free plan · No credit card required · 7-day money-back guarantee on paid plans --- --- title: skills.sh — the Agent Skills CLI | WebhookRelay meta: "og: title": "skills.sh — the Agent Skills CLI" description: skills.sh is the open-source CLI for installing Agent Skills into Claude and other agents. Use it to add the Webhook Relay skills with a single command. url: https://webhookrelay.com/docs/skills-cli.md file: /docs/skills-cli.md --- ![Stripes](https://webhookrelay.com/docs/skills-cli/images/stripes.svg) Documentation **Fundamentals** # **skills.sh — the Agent Skills CLI** skills.sh is the open-source CLI for installing Agent Skills into Claude and other agents. Use it to add the Webhook Relay skills with a single command. ## [What is skills.sh?](#what-is-skillssh) [skills.sh](https://www.skills.sh) is the open-source command-line tool for installing [Agent Skills](https://webhookrelay.com/docs/skills-cli/docs/skills/) — the small instruction packs that teach Claude (and other skill-aware agents) how to perform a specific task. You run it with `npx skills`, point it at a GitHub repository, and it downloads the skills and drops them into your agent's skills directory. In short, it: - **Installs skills from any GitHub repo** with one command — `npx skills add /`. - **Works per-project or globally** — keep skills next to a single project, or install with `-g` so every agent on your machine can use them. - **Is open source and free** — no account, no registry lock-in; it just pulls the skill files from the repository you point it at. It's the fastest way to install the Webhook Relay skills, which is why the [Agent Skills](https://webhookrelay.com/docs/skills-cli/docs/skills/) docs use it. ## [Add the Webhook Relay skills](#add-the-webhook-relay-skills) The Webhook Relay skills live in the open-source [webhookrelay/skills](https://github.com/webhookrelay/skills) repository. Add all of them with: ``` npx skills add webhookrelay/skills ``` Install globally so every agent on your machine picks them up: ``` npx skills add webhookrelay/skills -g ``` Or install just one skill with `--skill`: ``` npx skills add webhookrelay/skills --skill webhook-debug ``` Once installed, your agent can drive Webhook Relay directly — forwarding webhooks, transforming them, debugging incoming requests, opening tunnels, scheduling recurring webhooks, and receiving, parsing and transforming inbound email. See [Agent Skills](https://webhookrelay.com/docs/skills-cli/docs/skills/) for the full list of skills and what each one does. > Most skills use the `relay` CLI — see [CLI installation](https://webhookrelay.com/docs/skills-cli/docs/installation/cli/), then run `relay login`. The `webhook-debug` skill needs nothing but `curl`. ## [Other ways to install](#other-ways-to-install) skills.sh is one distribution channel. The same skills are also available via the Claude Code plugin marketplace, the [ClawHub](https://webhookrelay.com/docs/skills-cli/docs/clawhub/) registry, or a manual copy from the repo — see [Agent Skills](https://webhookrelay.com/docs/skills-cli/docs/skills/) for every option. ## [Links](#links) - [skills.sh](https://www.skills.sh) — the open-source Agent Skills CLI - [Agent Skills](https://webhookrelay.com/docs/skills-cli/docs/skills/) — all install channels and the full skill list - [Webhook Relay skills on GitHub](https://github.com/webhookrelay/skills) — browse the source and contribute - [Webhook Relay on ClawHub](https://webhookrelay.com/docs/skills-cli/docs/clawhub/) — install from the open skill registry - [MCP server](https://webhookrelay.com/docs/skills-cli/docs/mcp/) — give an agent live, typed tools against your account Did this page help you? --- --- title: A RequestBin Alternative: Free Webhook Inspector, No Signup meta: "og: title": "A RequestBin Alternative: Free Webhook Inspector, No Signup" description: The original free RequestBin is gone. Webhook Bin is a free alternative — an instant URL, real-time request inspection, then forwarding to localhost. url: https://webhookrelay.com/blog/requestbin-alternative.md file: /blog/requestbin-alternative.md --- ![Stripes](https://webhookrelay.com/blog/requestbin-alternative/images/stripes.svg) # **A RequestBin Alternative: Free Webhook Inspector, No Signup** The original free RequestBin is gone. Webhook Bin is a free alternative — an instant URL, real-time request inspection, then forwarding to localhost. ![A RequestBin Alternative: Free Webhook Inspector, No Signup](https://webhookrelay.com/blog/requestbin-alternative/images/blog/heroes/alternative.jpg) [RequestBin](https://requestbin.com) was the original "paste a URL, see the request" tool. The free hosted version was discontinued and folded into Pipedream, so the simple no-signup bin people remember now sits behind an account. If that's what sent you here, you want the same thing RequestBin used to be: a **free, instant, no-signup request bin**. That's exactly what Webhook Relay's [Webhook Bin](https://webhookrelay.com/blog/requestbin-alternative/webhook-bin/) is — plus a way to forward those requests to your own code. ## [TL;DR](#tldr) - **Want the classic free request bin?** [Webhook Bin](https://webhookrelay.com/blog/requestbin-alternative/webhook-bin/) gives you an instant URL with no signup. - **Want to inspect every detail?** Headers, query params, method and body, in real time. - **Want it to reach your app?** Forward to localhost or a private server with the relay agent. ## [RequestBin vs Webhook Bin](#requestbin-vs-webhook-bin) | | RequestBin (Pipedream) | Webhook Relay (Webhook Bin) | | --- | --- | --- | | Free, no-signup bin | Account required | Yes | | Instant URL | Yes | Yes | | Real-time inspection | Yes | Yes | | Custom response | Limited | Yes | | Forward to localhost / private server | No | Yes (relay agent) | | Transform / fan-out | Via workflows | [Native](https://webhookrelay.com/blog/requestbin-alternative/features/transform-webhooks/) | | Permanent endpoint | Paid | Yes (free account) | ## [How to use it](#how-to-use-it) 1. **Open the bin.** Go to [Webhook Bin](https://webhookrelay.com/blog/requestbin-alternative/webhook-bin/) — a unique URL is created instantly. 2. **Send a request.** Point any service or a quick `curl` at it: ``` curl -X POST https://your-bin-url \ -H 'Content-Type: application/json' \ -d '{"hello":"world"}' ``` 1. **Inspect.** The request appears immediately with full headers and body. 2. **(Optional) Forward it.** [Create a free account](https://my.webhookrelay.com/register), install the agent, and deliver requests to `localhost:8080` or any internal service. ## [Why developers move from a plain bin](#why-developers-move-from-a-plain-bin) A request bin shows you the payload. Webhook Relay also **delivers** it — so the same URL you debug with becomes the one your provider keeps calling, with [retries](https://webhookrelay.com/blog/requestbin-alternative/webhooks/), [transformations](https://webhookrelay.com/blog/requestbin-alternative/features/transform-webhooks/) and [multiple destinations](https://webhookrelay.com/blog/requestbin-alternative/features/webhook-multiple-destinations/) when you need them. [Open Webhook Bin](https://webhookrelay.com/blog/requestbin-alternative/webhook-bin/) or [create a free account](https://my.webhookrelay.com/register) to forward requests to your own machine. ![Stripes](https://webhookrelay.com/blog/requestbin-alternative/images/stripes-dark.svg) ## **Start forwarding webhooks in minutes ** Connect a source, pick a destination, and Webhook Relay handles delivery, retries and transforms. Set up your first webhook in under five minutes. [Start for free ->](https://my.webhookrelay.com/register) Free plan · No credit card required · 7-day money-back guarantee on paid plans --- --- title: Jenkins Plugin | WebhookRelay meta: "og: title": "Jenkins Plugin" description: Receive GitHub, GitLab and Bitbucket webhooks in Jenkins without a public IP using the native Webhook Relay plugin. url: https://webhookrelay.com/docs/tutorials/cicd/jenkins-plugin.md file: /docs/tutorials/cicd/jenkins-plugin.md --- ![Stripes](https://webhookrelay.com/docs/tutorials/cicd/jenkins-plugin/images/stripes.svg) Documentation **Fundamentals** # **Jenkins Plugin** Receive GitHub, GitLab and Bitbucket webhooks in Jenkins without a public IP using the native Webhook Relay plugin. The **Webhook Relay Jenkins plugin** lets Jenkins receive webhooks from GitHub, GitLab and Bitbucket **without exposing Jenkins to the public internet** — no public IP, no reverse proxy, no inbound firewall rules. The plugin opens an outbound connection to a Webhook Relay **bucket** and forwards each webhook it receives to the matching Jenkins endpoint (`/github-webhook/`, `/bitbucket-hook/`, …), so your SCM can trigger builds even when Jenkins runs on a private network, behind a NAT, or on your laptop. Unlike running the `relay` CLI as a separate agent, the plugin runs **inside Jenkins** — there is nothing else to install or keep alive. And because delivery goes through a bucket (the forwarding feature, not a tunnel), **every webhook is recorded on the bucket's logs page** together with the response Jenkins returned, which makes debugging deliveries trivial. ![GitHub to Jenkins without public IP](https://webhookrelay.com/docs/tutorials/cicd/jenkins-plugin/images/tutorials/jenkins/github-webhooks-jenkins.png) ## [How it works](#how-it-works) ``` GitHub / GitLab / Bitbucket │ (webhook) ▼ https://.hooks.webhookrelay.com ← public URL you paste into the SCM │ ▼ Webhook Relay bucket ──────────────► logs page (every request + Jenkins response) │ (outbound WebSocket opened by the plugin) ▼ Jenkins → /github-webhook/ → build triggered ``` 1. The plugin authenticates with your API token and **subscribes** to a bucket over a persistent outbound WebSocket. No inbound ports are opened on Jenkins. 2. A webhook hits the bucket's public input URL and Webhook Relay streams it down the socket. 3. The plugin replays the request against the Jenkins endpoint for your SCM and reports the response back to the bucket log. ## [Requirements](#requirements) - Jenkins 2.479.1 or newer (Java 17 or 21) - A free [Webhook Relay](https://webhookrelay.com) account and an [API token](https://my.webhookrelay.com/tokens) ## [1. Install the plugin](#_1-install-the-plugin) From **Manage Jenkins → Plugins → Available**, search for **Webhook Relay** and install it. To install a downloaded build manually, go to **Manage Jenkins → Plugins → Advanced settings → Deploy Plugin** and upload the `webhook-relay.hpi` file: ![Deploy the Webhook Relay plugin](https://webhookrelay.com/docs/tutorials/cicd/jenkins-plugin/images/tutorials/jenkins/plugin-01-install-deploy-plugin.png) After restarting, the plugin shows up under **Installed plugins**: ![Webhook Relay plugin installed](https://webhookrelay.com/docs/tutorials/cicd/jenkins-plugin/images/tutorials/jenkins/plugin-02-installed-plugin.png) ## [2. Configure the connection](#_2-configure-the-connection) Open **Manage Jenkins → System** and scroll to the **Webhook Relay** section: 1. Enter your **API Key** and **API Secret** from the [tokens page](https://my.webhookrelay.com/tokens). 2. Enter a **Bucket name** (for example `jenkins-plugin`). It can be an existing bucket or a new name — the plugin will create it for you. 3. Pick your **SCM Webhook Preset** (GitHub, GitLab, Bitbucket or Generic Webhook Trigger). 4. Tick **Enable** and click **Save**. ![Configure the Webhook Relay plugin](https://webhookrelay.com/docs/tutorials/cicd/jenkins-plugin/images/tutorials/jenkins/plugin-03-config-subscribed.png) When connected, **Connection Status** reads **✔ Subscribed**. Use **Test Connection** to verify your credentials at any time. ## [3. Get your webhook URL](#_3-get-your-webhook-url) Click **Get Webhook URL**. The plugin resolves the bucket's public input URL (creating the bucket if it doesn't exist yet) and shows the value to paste into your repository: ![Resolve the public webhook URL](https://webhookrelay.com/docs/tutorials/cicd/jenkins-plugin/images/tutorials/jenkins/plugin-04-get-webhook-url.png) Add it as a webhook in your repository settings: | SCM | Where to paste the URL | | --- | --- | | GitHub | _Settings → Webhooks → Add webhook → Payload URL_ (content type `application/json`) | | GitLab | _Settings → Webhooks → URL_ | | Bitbucket | _Repository settings → Webhooks → Add webhook → URL_ | That's the only change in your SCM — you point the webhook at the Webhook Relay URL instead of at Jenkins directly. ## [4. Trigger a build](#_4-trigger-a-build) Configure the Jenkins job exactly as you would for a public Jenkins. For GitHub, that means a Pipeline or Freestyle job with a Git SCM and **GitHub hook trigger for GITScm polling** enabled (`githubPush()` in a declarative pipeline). For GitLab and Bitbucket, enable the corresponding hook trigger; for everything else, use the [Generic Webhook Trigger](https://plugins.jenkins.io/generic-webhook-trigger/). Now push a commit. The webhook travels GitHub → bucket → plugin → `/github-webhook/`, and the build starts: ![Build triggered by a GitHub push through Webhook Relay](https://webhookrelay.com/docs/tutorials/cicd/jenkins-plugin/images/tutorials/jenkins/plugin-05-build-triggered.png) ## [5. Multibranch Pipelines](#_5-multibranch-pipelines) Multibranch Pipeline projects work too, but they are configured differently — **a Multibranch Pipeline has no _Build Triggers_ section**. Branch and pull request jobs are created and triggered by the branch source (e.g. [GitHub Branch Source](https://plugins.jenkins.io/github-branch-source/)) reacting to the webhook events the plugin delivers to the same endpoints. The full setup — branch builds, automatic jobs for new branches and pull requests — has its own page: [Jenkins Multibranch Pipelines](https://webhookrelay.com/docs/tutorials/cicd/jenkins-plugin/docs/tutorials/cicd/jenkins-plugin-multibranch/). ## [6. Inspect deliveries](#_6-inspect-deliveries) Open your bucket on [my.webhookrelay.com/buckets](https://my.webhookrelay.com/buckets). Each webhook is listed with its method, status and the response Jenkins returned — a `sent` status with response status `200` means Jenkins accepted the webhook. ## [Try it with Docker](#try-it-with-docker) The plugin repository ships a ready-to-run demo (Jenkins + Configuration as Code): ``` git clone https://github.com/jenkinsci/webhook-relay-plugin cd webhook-relay-plugin mvn -q clean package docker compose -f demo/docker-compose.yml up --build # Jenkins on http://localhost:8095 ``` Then follow the steps above. ## [Prefer the CLI agent?](#prefer-the-cli-agent) If you'd rather run a standalone forwarding agent instead of installing a plugin, see [Jenkins and GitHub](https://webhookrelay.com/docs/tutorials/cicd/jenkins-plugin/docs/tutorials/cicd/jenkins-github/) and [Jenkins and Bitbucket](https://webhookrelay.com/docs/tutorials/cicd/jenkins-plugin/docs/tutorials/cicd/jenkins-bitbucket/), which use the `relay` CLI. The plugin and the CLI agent both deliver through buckets, so the logs and SCM setup are identical — the plugin just removes the separate agent process. Did this page help you? --- --- title: A smee.io Alternative for Reliable Webhook Forwarding meta: "og: title": "A smee.io Alternative for Reliable Webhook Forwarding" description: smee.io is great for quick GitHub tests but is dev-only. Webhook Relay is a production-ready alternative: stable URLs, retries, and private delivery. url: https://webhookrelay.com/blog/smee-io-alternative.md file: /blog/smee-io-alternative.md --- ![Stripes](https://webhookrelay.com/blog/smee-io-alternative/images/stripes.svg) # **A smee.io Alternative for Reliable Webhook Forwarding** smee.io is great for quick GitHub tests but is dev-only. Webhook Relay is a production-ready alternative: stable URLs, retries, and private delivery. ![A smee.io Alternative for Reliable Webhook Forwarding](https://webhookrelay.com/blog/smee-io-alternative/images/blog/heroes/alternative.jpg) [smee.io](https://smee.io) is the webhook proxy from the GitHub/Probot ecosystem. It's a handy way to receive GitHub webhooks on your laptop while building a bot or an integration — paste the channel URL, run the client, done. But smee.io is **explicitly meant for development**, not production, and it offers no retries, persistence, stable guarantees or support. Webhook Relay does the same localhost forwarding, but is built to be **relied on**. ## [TL;DR](#tldr) - **Hacking on a Probot app or a quick GitHub integration?** smee.io is fine. - **Need it to keep working** — with retries, a URL that won't disappear, and delivery to a private CI server? Use Webhook Relay. ## [smee.io vs Webhook Relay](#smeeio-vs-webhook-relay) | | smee.io | Webhook Relay | | --- | --- | --- | | Forward webhooks to localhost | Yes | Yes | | Stable, persistent URL | Best-effort | Yes | | Retries on failure | No | Yes | | Request inspection UI | Basic | [Webhook Bin](https://webhookrelay.com/blog/smee-io-alternative/webhook-bin/) | | Transform payloads | No | [Yes](https://webhookrelay.com/blog/smee-io-alternative/features/transform-webhooks/) | | Deliver to private server / Kubernetes | No | Yes | | Authentication on the endpoint | No | Yes | | Intended for production | No (dev only) | Yes | | Support / SLA | Community | Paid plans | ## [The same five-minute setup, but production-grade](#the-same-five-minute-setup-but-production-grade) GitHub → Webhook Relay → your machine: 1. [Create a free account](https://my.webhookrelay.com/register) and note your endpoint URL. 2. Add it as a webhook in your GitHub repo (Settings → Webhooks). 3. Run the agent to deliver events to your local server or CI: ``` relay forward --bucket github http://localhost:3000/github/webhook ``` Because the URL is stable and deliveries retry, you can leave it running — for a dev box _or_ a real internal CI server behind a firewall. See the [GitHub + Jenkins guide](https://webhookrelay.com/blog/smee-io-alternative/blog/github-jenkins-guide/) for a CI example. ## [Why move off smee.io for anything real](#why-move-off-smeeio-for-anything-real) smee.io is a great convenience for the Probot tutorial. The moment a missed delivery matters — a CI build that didn't trigger, a bot that went quiet — you want **retries, persistence and visibility**. That's the line between a dev helper and infrastructure. [Start free](https://my.webhookrelay.com/register), or [inspect a GitHub webhook](https://webhookrelay.com/blog/smee-io-alternative/webhook-bin/) in your browser first. ![Stripes](https://webhookrelay.com/blog/smee-io-alternative/images/stripes-dark.svg) ## **Start forwarding webhooks in minutes ** Connect a source, pick a destination, and Webhook Relay handles delivery, retries and transforms. Set up your first webhook in under five minutes. [Start for free ->](https://my.webhookrelay.com/register) Free plan · No credit card required · 7-day money-back guarantee on paid plans --- --- title: Terraform Atlantis | WebhookRelay meta: "og: title": "Terraform Atlantis" description: Securely forward webhooks to Terraform Atlantis in Kubernetes cluster using Webhook Relay Operator url: https://webhookrelay.com/docs/tutorials/cicd/terraform-atlantis.md file: /docs/tutorials/cicd/terraform-atlantis.md --- ![Stripes](https://webhookrelay.com/docs/tutorials/cicd/terraform-atlantis/images/stripes.svg) Documentation **Fundamentals** # **Terraform Atlantis** Securely forward webhooks to Terraform Atlantis in Kubernetes cluster using Webhook Relay Operator ![Atlantis with Terraform](https://webhookrelay.com/docs/tutorials/cicd/terraform-atlantis/images/tutorials/atlantis/atlantis_with_whr.png) [Atlantis](https://www.runatlantis.io/) is an [open source](https://github.com/runatlantis/atlantis) [Terraform](https://www.terraform.io/) pull request automation tool. It works by receiving & processing webhooks from various source control management systems such as GitHub, GitLab, Bitbucket, etc. A full list of webhook providers can be found in Atlantis [official docs](https://www.runatlantis.io/docs/configuring-webhooks.html). In this tutorial, we will deploy Atlantis in a Kubernetes cluster that doesn't have public access. > Note that while we are using Helm to install both Webhook Relay and Atlantis services, this can be achieved with normal Kubernetes manifests. ## [Prerequisites](#prerequisites) - [Kubernetes](https://kubernetes.io) environment and kubectl configured on your machine. For this tutorial, I will be using [Minikube](https://kubernetes.io/docs/setup/learning-environment/minikube/) on my local machine but the same instructions will work on any other Kubernetes cluster such as GKE or EKS. - [Helm](https://helm.sh) CLI - a Kubernetes package manager. - [Webhook Relay account](https://my.webhookrelay.com/) - webhook forwarding solution to our internal Kubernetes environment. - [GitHub](https://github.com) ## [Git host access credentials](#git-host-access-credentials) Atlantis will need to communicate with our Git hosting provider. Follow [official documentation](https://www.runatlantis.io/docs/access-credentials.html#create-an-atlantis-user-optional) on getting access token that we can later use in the installation. For GitHub steps are: 1. Create a Personal Access Token by following: [https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token#creating-a-token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token#creating-a-token) 2. Create the token with repo scope Save the access token to your notepad or password manager, we will need it in the next step. ## [Installation](#installation) We will be using the official Atlantis [Helm](https://helm.sh) chart that can be found here: [https://hub.kubeapps.com/charts/stable/atlantis](https://hub.kubeapps.com/charts/stable/atlantis). Let's start by creating a namespace: ``` kubectl create ns atlantis ``` And let's switch context to it: ``` kubectl config set-context $(kubectl config current-context) --namespace=atlantis ``` ### [Atlantis](#atlantis) Add repositories: ``` helm repo add stable https://kubernetes-charts.storage.googleapis.com helm repo update ``` And to install the Atlantis chart (don't forget to set your own details in `github.user`, `github.token` and `github.secret`): ``` helm upgrade --install atlantis stable/atlantis --version 3.12.2 \ --set=github.user=rusenask \ --set=github.token=XXX \ --set=github.secret=very-secret \ --set=service.type=ClusterIP \ --set=ingress.enabled=false \ --set=orgWhitelist="github.com/webhookrelay/*" ``` Here, variables are: - **github.user** - your GitHub username - **github.token** - your GitHub personal access token - **github.secret** - shared secret that will have to be shared with Atlantis - **service.type** - making sure Atlantis is not exposed as a NodePort service - **ingress.enabled** - disabling ingress - **orgWhitelist** - which repositories should be processed To verify your installation, check pods: ``` kubectl get pods NAME READY STATUS RESTARTS AGE atlantis-0 1/1 Running 0 95s ``` And services: ``` kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE atlantis ClusterIP 10.99.122.187 80/TCP 9s ``` ### [Webhook Relay operator](#webhook-relay-operator) Once Atlantis is deployed, we will need to create a Webhook Relay operator which will ensure that webhooks will get delivered to our Atlantis pod. Retrieve your access token key & secret pair from [https://my.webhookrelay.com/tokens](https://my.webhookrelay.com/tokens) and set them as an environment variables: ``` export RELAY_KEY=xxxxxxxxxxxx export RELAY_SECRET=xxxxx ``` Add Webhook Relay Operator Helm repository: ``` helm repo add webhookrelay https://charts.webhookrelay.com helm repo update ``` Install it: ``` helm upgrade --install webhookrelay-operator webhookrelay/webhookrelay-operator \ --set credentials.key=$RELAY_KEY \ --set credentials.secret=$RELAY_SECRET ``` We should see two pods running in our **atlantis** namespace: ``` kubectl get pods NAME READY STATUS RESTARTS AGE atlantis-0 1/1 Running 0 6m32s webhookrelay-operator-65655d7c95-pvtnw 1/1 Running 0 13s ``` > Operator doesn't forward webhooks on its own. Each created CR will ensure an agent deployment that is configured to route specific buckets. Next step is to configure webhook forwarding to the atlantis service: ``` apiVersion: forward.webhookrelay.com/v1 kind: WebhookRelayForward metadata: name: forward-to-atlantis spec: buckets: - name: github-to-atlantis inputs: - name: public-endpoint description: "Endpoint for GitHub" responseBody: "OK" responseStatusCode: 200 outputs: - name: atlantis-pod destination: http://atlantis:80 ``` Save this file as `whr-atlantis-cr.yaml` and create it: ``` kubectl apply -f whr-atlantis-cr.yaml webhookrelayforward.forward.webhookrelay.com/forward-to-atlantis created ``` You should see a new pod running in your cluster: ``` kubectl get pods NAME READY STATUS RESTARTS AGE atlantis-0 1/1 Running 0 11m forward-to-atlantis-whr-deployment-c9bf7fcd7-tz6zq 1/1 Running 0 14s webhookrelay-operator-65655d7c95-pvtnw 1/1 Running 0 4m43s ``` Run a `kubectl describe` on our CRD: ``` kubectl describe webhookrelayforwards.forward.webhookrelay.com forward-to-atlantis Name: forward-to-atlantis Namespace: atlantis Labels: Annotations: API Version: forward.webhookrelay.com/v1 Kind: WebhookRelayForward Metadata: Creation Timestamp: 2020-08-11T10:50:26Z Generation: 1 Resource Version: 1582466 Self Link: /apis/forward.webhookrelay.com/v1/namespaces/atlantis/webhookrelayforwards/forward-to-atlantis UID: fbee4016-787d-4b3b-8915-d6c104f4b88c Spec: Buckets: Inputs: Description: Endpoint for GitHub Name: public-endpoint Response Body: OK Response Status Code: 200 Name: github-to-atlantis Outputs: Destination: http://atlantis:80 Name: atlantis-pod Status: Agent Status: Running Public Endpoints: https://o0iwkgx6phxnun8wifpxij.hooks.webhookrelay.com Ready: true Routing Status: Configured Events: ``` Here we can see our public webhooks URL " [https://o0iwkgx6phxnun8wifpxij.hooks.webhookrelay.com](https://o0iwkgx6phxnun8wifpxij.hooks.webhookrelay.com)". Find yours and let's add it to the GitHub. You can also view your bucket configuration and agent connection status through [buckets dashboard](https://my.webhookrelay.com/buckets): ![Buckets list](https://webhookrelay.com/docs/tutorials/cicd/terraform-atlantis/images/tutorials/atlantis/buckets.png) ### [Git host configuration](#git-host-configuration) GitHub configuration instructions can be found here [https://www.runatlantis.io/docs/configuring-webhooks.html#github-github-enterprise](https://www.runatlantis.io/docs/configuring-webhooks.html#github-github-enterprise). However, if you are using any other git hosting provider that Atlantis supports, follow those steps. > Note that my instance endpoint is ' [https://o0iwkgx6phxnun8wifpxij.hooks.webhookrelay.com](https://o0iwkgx6phxnun8wifpxij.hooks.webhookrelay.com)' (yours will be different). Also, custom domains are available if you don't like the generated one. If you're installing on the organization, navigate to your organization's page, and click Settings. If installing on a single repository, navigate to the repository home page and click Settings. 1. Select **Webhooks** or **Hooks** in the sidebar 2. Click **Add webhook** 3. set **Payload URL** to `http://$WEBHOOKRELAY_URL/events` (or `https://$WEBHOOKRELAY_URL/events` if you're using SSL) where `$WEBHOOKRELAY_URL` is Webhook Relay public URL (in my example it's ' [https://o0iwkgx6phxnun8wifpxij.hooks.webhookrelay.com](https://o0iwkgx6phxnun8wifpxij.hooks.webhookrelay.com)'). Be sure to add /events 4. double-check you added **/events** to the end of your URL. 5. Set Content type to **application/json** 6. Set **Secret** to the **Webhook Secret** you set previously when installing Atlantis helm chart > Note that if you're adding a webhook to multiple repositories, each repository will need to use the same secret. 1. Select **Let me select individual events** 2. Check the boxes - **Pull request reviews** - **Pushes** - **Issue comments** - **Pull requests** 1. Leave **Active** checked 2. Click **Add webhook** Payload URL and Content Type should look something like this: ![Webhook Configuration](https://webhookrelay.com/docs/tutorials/cicd/terraform-atlantis/images/tutorials/atlantis/webhook_config.png) ## [Trying it out](#trying-it-out) Create a new change in GitHub in a file such as `main.tf` with a resource `resource "null_resource" "example-1" {}`. Atlantis will receive an event through Webhook Relay and create a plan: ![Atlantis plan on PR](https://webhookrelay.com/docs/tutorials/cicd/terraform-atlantis/images/tutorials/atlantis/pr_plan.png) Did this page help you? --- --- title: Convoy Alternative for Webhook Delivery | WebhookRelay meta: "og: title": "Convoy Alternative for Webhook Delivery" description: Compare Convoy and Webhook Relay. Convoy is a self-hostable webhook gateway; Webhook Relay is hosted and also delivers to localhost and private networks. url: https://webhookrelay.com/blog/convoy-alternative.md file: /blog/convoy-alternative.md --- ![Stripes](https://webhookrelay.com/blog/convoy-alternative/images/stripes.svg) # **A Convoy Alternative for Delivering Webhooks Into Private Infrastructure** Compare Convoy and Webhook Relay. Convoy is a self-hostable webhook gateway; Webhook Relay is hosted and also delivers to localhost and private networks. ![A Convoy Alternative for Delivering Webhooks Into Private Infrastructure](https://webhookrelay.com/blog/convoy-alternative/images/blog/heroes/alternative.jpg) [Convoy](https://www.getconvoy.io) (getconvoy.io) is an open-source, self-hostable webhook gateway, built in Go and backed by Y Combinator. It handles both inbound and outbound webhooks, with features like retries, rate limiting, static outbound IPs and circuit breaking. If you're evaluating it against Webhook Relay, the key difference is the **operating model**: Convoy is software you run and maintain yourself, while Webhook Relay is a hosted service that can also deliver webhooks into private infrastructure that has no public IP. ## [TL;DR](#tldr) - **Want a self-hosted open-source gateway and willing to run and maintain it yourself?** Convoy is built for that and is a solid choice. - **Want a fully managed, hosted webhook service with no infrastructure to operate?** That's Webhook Relay. - **Need delivery to localhost, an internal service or a Kubernetes pod?** Webhook Relay's relay agent does this natively; Convoy delivers to HTTP endpoints. - **Want tunnels, scheduled/cron webhooks and a free request bin too?** Webhook Relay includes all three. ## [Convoy vs Webhook Relay](#convoy-vs-webhook-relay) | | Convoy | Webhook Relay | | --- | --- | --- | | Operating model | Self-hosted open source | Hosted service | | License / source | Open source (MPL 2.0, as of 2026) | Hosted (closed) | | Inbound + outbound webhooks | Yes | Yes | | Retries, rate limiting, circuit breaking | Yes | Retries + ordered delivery | | Static outgoing IP | Yes (outbound proxy) | [Yes](https://webhookrelay.com/blog/convoy-alternative/features/static-outgoing-ip/) | | Deliver to public endpoints | Yes | [Yes](https://webhookrelay.com/blog/convoy-alternative/features/webhook-multiple-destinations/) | | Deliver to **localhost / private network** | No (HTTP endpoints) | Yes ([relay agent](https://webhookrelay.com/blog/convoy-alternative/webhooks/)) | | General-purpose tunnels | No | [Yes](https://webhookrelay.com/blog/convoy-alternative/tunnels/) | | Scheduled / cron webhooks | No | [Yes](https://webhookrelay.com/blog/convoy-alternative/cron/) | | Transformations | Yes | [JS / Lua + AI](https://webhookrelay.com/blog/convoy-alternative/features/transform-webhooks/) | | Kubernetes operator | Run it yourself | [Yes](https://webhookrelay.com/blog/convoy-alternative/features/webhook-kubernetes-integration/) | | Consumer-facing free request bin | No | [Webhook Bin](https://webhookrelay.com/blog/convoy-alternative/webhook-bin/) | | Hosting / ops burden | You run and maintain it | Fully managed | | Starting paid price (managed) | Self-host / cloud (verify status) | $9.99/mo | _Competitor details reflect publicly documented information as of 2026; verify current license, status and features on getconvoy.io and GitHub._ ## [Where Convoy is strong](#where-convoy-is-strong) - **Open source and self-hostable.** Convoy is open source (MPL 2.0 as of 2026) and you can run the whole gateway on your own infrastructure. If avoiding a third-party SaaS for compliance, data-residency or cost reasons matters to you, that's a real advantage. - **A proper webhook gateway.** It handles both directions — ingesting webhooks from providers and sending webhooks out — with retries, rate limiting, circuit breaking and rolling secrets, which is a thoughtful feature set for a platform team. - **Static outbound IPs.** Convoy supports routing outbound webhook events through a forward proxy with a static IP, useful when a receiving system needs to allow-list your sender. - **You own the deployment.** Because it's your instance, you control scaling, retention and configuration end to end. If you specifically want a self-hosted open-source gateway and you're prepared to run and maintain it yourself, Convoy is a reasonable pick and we'd happily point you there. A practical caveat: as of 2026, Convoy's cloud offering and overall project momentum appear to have slowed. Verify its current status, roadmap and uptime on getconvoy.io and GitHub before relying on the hosted version in production. ## [Where Webhook Relay is different](#where-webhook-relay-is-different) ### [It's fully managed, not self-hosted](#its-fully-managed-not-self-hosted) The most basic distinction is that Webhook Relay is a hosted service. There's no gateway to deploy, no database to operate, no upgrade path to babysit, and no on-call rotation for the webhook layer itself. You point providers at a stable URL and run a lightweight agent where you need delivery. With Convoy you own all of that operational surface. ### [Delivery into private networks](#delivery-into-private-networks) This is the core technical difference. Convoy delivers to HTTP endpoints; the receiving service has to be reachable from the gateway. Webhook Relay's relay agent instead makes an **outbound** connection and forwards webhooks to wherever you point it: ``` relay forward --bucket payments http://localhost:8080/stripe # or an internal host with no public IP relay forward --bucket payments http://payments.internal:9000/hook ``` No inbound firewall rules, no public IP required, including a first-class [Kubernetes operator](https://webhookrelay.com/blog/convoy-alternative/features/webhook-kubernetes-integration/) for delivering into your cluster. This is closer to a secure outbound tunnel into your own infrastructure than to endpoint-to-endpoint delivery. ### [Tunnels, cron and a free request bin](#tunnels-cron-and-a-free-request-bin) Webhook Relay also offers general-purpose [localhost tunnels](https://webhookrelay.com/blog/convoy-alternative/tunnels/) for exposing any local service to the internet — not just webhooks — which Convoy doesn't do. It runs [scheduled (cron) webhooks](https://webhookrelay.com/blog/convoy-alternative/cron/) natively, useful for heartbeats and periodic jobs. And the free [Webhook Bin](https://webhookrelay.com/blog/convoy-alternative/webhook-bin/) lets anyone inspect incoming requests in the browser with no account required, a consumer-facing inspector Convoy doesn't ship. ### [Fan-out, transformations and static IP](#fan-out-transformations-and-static-ip) Like Convoy, Webhook Relay can [fan a single webhook out to multiple destinations](https://webhookrelay.com/blog/convoy-alternative/features/webhook-multiple-destinations/), reshape payloads in flight with [JavaScript or Lua transformations](https://webhookrelay.com/blog/convoy-alternative/features/transform-webhooks/), and give you a [static outgoing IP](https://webhookrelay.com/blog/convoy-alternative/features/static-outgoing-ip/) for allow-listing — but without you running and scaling the gateway yourself. ## [When to pick which](#when-to-pick-which) - **Pick Convoy** if you specifically want a self-hosted, open-source webhook gateway, you're comfortable running and maintaining it, and self-hosting matters more to you than a managed service. Verify its current status before committing. - **Pick Webhook Relay** if you want a fully managed hosted service — especially delivery into localhost, internal services or Kubernetes — plus tunnels, cron and a free request bin, without operating any infrastructure, starting at $9.99/month. [Start free](https://my.webhookrelay.com/register) or [compare plans](https://webhookrelay.com/blog/convoy-alternative/pricing/). New to webhooks? [Inspect one in the browser](https://webhookrelay.com/blog/convoy-alternative/webhook-bin/) first. ![Stripes](https://webhookrelay.com/blog/convoy-alternative/images/stripes-dark.svg) ## **Start forwarding webhooks in minutes ** Connect a source, pick a destination, and Webhook Relay handles delivery, retries and transforms. Set up your first webhook in under five minutes. [Start for free ->](https://my.webhookrelay.com/register) Free plan · No credit card required · 7-day money-back guarantee on paid plans --- --- title: localtunnel Alternative for Webhooks (2026) | WebhookRelay meta: "og: title": "localtunnel Alternative for Webhooks (2026)" description: Compare localtunnel and Webhook Relay for exposing localhost and forwarding webhooks — stable URLs, retries, request inspection, and a free plan. url: https://webhookrelay.com/blog/localtunnel-alternative.md file: /blog/localtunnel-alternative.md --- ![Stripes](https://webhookrelay.com/blog/localtunnel-alternative/images/stripes.svg) # **A Reliable localtunnel Alternative for Webhooks and Tunnels (2026)** Compare localtunnel and Webhook Relay for exposing localhost and forwarding webhooks — stable URLs, retries, request inspection, and a free plan. ![A Reliable localtunnel Alternative for Webhooks and Tunnels (2026)](https://webhookrelay.com/blog/localtunnel-alternative/images/blog/heroes/alternative.jpg) If you searched for a **localtunnel alternative**, you have probably already met its rough edges: the public instance returning 502s, the tunnel dropping mid-session, or the random URL that changes every time you restart. [localtunnel](https://github.com/localtunnel/localtunnel) is a genuinely useful free, open-source tool — `npx localtunnel --port 3000` and you have a public URL — but as of 2026 (verify current details) the hosted instance is best-effort and the project sees little active maintenance. Webhook Relay approaches the same problem from the webhook side: a stable public URL, request inspection, retries, and the ability to forward, transform and fan-out webhooks to localhost or any private server. ## [TL;DR](#tldr) - **Need something that stays up?** localtunnel's public instance is best-effort and unmaintained as of 2026; Webhook Relay is a maintained, hosted service with a free plan. - **Tired of the URL changing on restart?** Webhook Relay's endpoint URL is stable on every plan, including free — localtunnel's subdomains are random. - **Testing provider webhooks (Stripe, GitHub, Shopify)?** Use the free [Webhook Bin](https://webhookrelay.com/blog/localtunnel-alternative/webhook-bin/) to inspect them in your browser, then forward to localhost with the agent. - **localtunnel is still the better pick** for a quick, throwaway demo where reliability genuinely doesn't matter and you want zero accounts. ## [localtunnel vs Webhook Relay at a glance](#localtunnel-vs-webhook-relay-at-a-glance) | | localtunnel | Webhook Relay | | --- | --- | --- | | Cost | Free, open source | Free plan, paid from $9.99/mo | | Stable URL on restart | No (random subdomain) | Yes, every plan | | Reliability / maintenance | Best-effort, low maintenance (2026) | Maintained hosted service | | Inspect requests | No built-in inspector | Yes ([Webhook Bin](https://webhookrelay.com/blog/localtunnel-alternative/webhook-bin/)) | | Forward to localhost | Yes (it's a tunnel) | Yes (via the [relay agent](https://webhookrelay.com/blog/localtunnel-alternative/tunnels/)) | | Forward to a private network / Kubernetes | No (just the tunnel host) | Yes | | Transform payloads (JS/Lua) | No | [Yes](https://webhookrelay.com/blog/localtunnel-alternative/features/transform-webhooks/) | | Fan-out to multiple destinations | No | [Yes](https://webhookrelay.com/blog/localtunnel-alternative/features/webhook-multiple-destinations/) | | Retries on failure | No | Yes | | Auth / SLA / support | None | Yes, on paid plans | _Competitor details reflect publicly documented behaviour as of 2026 and can change — verify localtunnel's current status before deciding._ ## [Where localtunnel shines](#where-localtunnel-shines) Let's be fair. localtunnel is good at exactly what it set out to do: - **It's free and open source.** No account, no card, no quota to read. - **Zero-config.** One command and you have a public URL — nothing to install permanently if you use `npx`. - **Self-hostable.** Because the server is open source, you can run your own instance if you want full control. For a quick throwaway demo — showing a teammate a page on your laptop for ten minutes — localtunnel is hard to beat on simplicity. ## [Where Webhook Relay wins for webhooks](#where-webhook-relay-wins-for-webhooks) ### [1. A URL you configure once](#_1-a-url-you-configure-once) The most common localtunnel frustration is the **changing subdomain**. Every restart hands you a new random URL, so you re-paste it into every provider's webhook settings. With Webhook Relay, your endpoint URL is **fixed** — set it once in Stripe or GitHub and forget it. That alone makes it usable for real webhook testing rather than one-off demos. ### [2. Reliability you can lean on](#_2-reliability-you-can-lean-on) As of 2026 (verify current details), the public localtunnel instance is widely reported to drop connections, return 502s, and rate-limit, with the project seeing little ongoing maintenance. Webhook Relay is a maintained, hosted service: the agent reconnects automatically, paid plans include support and an SLA, and there's no 2-hour clock to babysit. ### [3. Forward to localhost _and_ private networks](#_3-forward-to-localhost-and-private-networks) localtunnel exposes the machine running the client. Webhook Relay **routes a webhook** to wherever it needs to go — your laptop on `localhost:8080`, an internal API behind a firewall, or a Kubernetes service with no public IP: ``` # Install the agent, then forward your public endpoint to a local port relay forward --bucket my-app http://localhost:8080/webhook ``` The agent makes an **outbound** connection, so there are no firewall ports to open. You can also spin up a general-purpose [tunnel](https://webhookrelay.com/blog/localtunnel-alternative/tunnels/) when you need to expose an arbitrary local service. ### [4. Do something to the webhook in flight](#_4-do-something-to-the-webhook-in-flight) Because Webhook Relay sits in the path, you can [transform payloads](https://webhookrelay.com/blog/localtunnel-alternative/features/transform-webhooks/) with JavaScript or Lua (turn a raw GitHub event into a Slack message), [fan-out to multiple destinations](https://webhookrelay.com/blog/localtunnel-alternative/features/webhook-multiple-destinations/), filter noisy events, add authentication, or retry on failure. A plain tunnel like localtunnel can't do any of that — it just moves bytes. ### [5. A real free tier for testing](#_5-a-real-free-tier-for-testing) Open [Webhook Bin](https://webhookrelay.com/blog/localtunnel-alternative/webhook-bin/), get an instant URL, and watch requests arrive in real time — no signup, no install. When you're ready to forward them somewhere, [create a free account](https://my.webhookrelay.com/register) and install the agent. ## [How to switch from localtunnel in 2 minutes](#how-to-switch-from-localtunnel-in-2-minutes) 1. **Inspect first (no install):** open [Webhook Bin](https://webhookrelay.com/blog/localtunnel-alternative/webhook-bin/), copy the URL, and point your provider at it. 2. **Forward to localhost:** [create a free account](https://my.webhookrelay.com/register), install the agent, and run `relay forward`. 3. **Keep the URL forever:** your endpoint doesn't change, so you never re-configure the provider. ## [When to pick which](#when-to-pick-which) - **Pick localtunnel** for a quick throwaway demo where reliability doesn't matter, you want zero accounts, and you don't need any webhook features. - **Pick Webhook Relay** when the work is webhooks: stable URLs, a service that stays up, forwarding to private infrastructure, transforming and fanning-out events, and retries when a delivery fails. Ready to stop restarting tunnels and re-pasting URLs? [Start forwarding for free](https://my.webhookrelay.com/register) or [test a webhook now](https://webhookrelay.com/blog/localtunnel-alternative/webhook-bin/). ![Stripes](https://webhookrelay.com/blog/localtunnel-alternative/images/stripes-dark.svg) ## **Start forwarding webhooks in minutes ** Connect a source, pick a destination, and Webhook Relay handles delivery, retries and transforms. Set up your first webhook in under five minutes. [Start for free ->](https://my.webhookrelay.com/register) Free plan · No credit card required · 7-day money-back guarantee on paid plans --- --- title: Jenkins and Bitbucket | WebhookRelay meta: "og: title": "Jenkins and Bitbucket" description: A quick guide on Jenkins Bitbucket webhooks integration without public IP/NAT or behind a firewall url: https://webhookrelay.com/docs/tutorials/cicd/jenkins-bitbucket.md file: /docs/tutorials/cicd/jenkins-bitbucket.md --- ![Stripes](https://webhookrelay.com/docs/tutorials/cicd/jenkins-bitbucket/images/stripes.svg) Documentation **Fundamentals** # **Jenkins and Bitbucket** A quick guide on Jenkins Bitbucket webhooks integration without public IP/NAT or behind a firewall ## [How to Configure Bitbucket Webhooks with Jenkins](#how-to-configure-bitbucket-webhooks-with-jenkins) To configure Bitbucket webhooks with Jenkins, install the Jenkins Bitbucket plugin, set up webhook forwarding using Webhook Relay to handle the connection if Jenkins is behind a firewall, configure the webhook in your Bitbucket repository settings, and enable the build trigger in your Jenkins job. This allows automated builds whenever code is pushed to Bitbucket. In this tutorial, we will show a Jenkins Bitbucket integration using webhooks. It will work behind a firewall, inside a private network. You can use this setup for other services too - such as GitHub, GitLab or anything else that emits webhooks. Main advantages of Webhook Relay here are: - No delay between polling requests - Bitbucket webhooks are well-documented and easy to configure with Jenkins plugin - Additional security layer as Jenkins is not exposed to the internet as webhooks by default are uni-directional, responses are not returned to the caller. ![Github to Jenkins without public IP](https://webhookrelay.com/docs/tutorials/cicd/jenkins-bitbucket/images/tutorials/bitbucket/bitbucket-to-jenkins-narrow.png) ## [Prerequisites](#prerequisites) - Webhook Relay account, create one [here](https://my.webhookrelay.com). - Jenkins instance. We will not go into installing Jenkins itself as there are quite a few options and many articles on that. See Jenkins [official docs](https://www.jenkins.io/doc/book/installing/) for up-to-date instructions. - [Bitbucket](https://bitbucket.com) account and a repository that you will want to use. ## [Configure webhook forwarding](#configure-webhook-forwarding) We will be using the [Jenkins Bitbucket plugin](https://plugins.jenkins.io/bitbucket/). This plugin exposes a single endpoint to which we can send bitbucket webhooks from multiple repositories. Go to the internal URL forwarding setup page [https://my.webhookrelay.com/new-internal-destination](https://my.webhookrelay.com/new-internal-destination) and enter your Jenkins address. In my case I will be running the agent on the same machine as Jenkins so the address for me is `http://localhost:8080/bitbucket-hook/`: ![Webhook Relay internal forwarding configuration helper](https://webhookrelay.com/docs/tutorials/cicd/jenkins-bitbucket/images/tutorials/bitbucket/internal-config.png) Follow the instructions to setup the agent and being forwarding webhooks. You will get your public URL that you can use in Bitbucket webhook configuration. ![Our public URL is 'https://aqjftr6vxxtfrjfrvcqrku.hooks.webhookrelay.com'](https://webhookrelay.com/docs/tutorials/cicd/jenkins-bitbucket/images/tutorials/bitbucket/public-urls.png) As you can see in the screenshot above, take the "Listening on" address. ## [Configure Bitbucket](#configure-bitbucket) For Bitbucket webhook configuration you can follow the plugin guide here: [https://plugins.jenkins.io/bitbucket/#plugin-content-bitbucket-cloud-usage](https://plugins.jenkins.io/bitbucket/#plugin-content-bitbucket-cloud-usage). You need to go to the repository settings and then to the webhooks section add "Add webhook" with the public URL that you have gotten from the previous step: ![Bitbucket webhook configuration section](https://webhookrelay.com/docs/tutorials/cicd/jenkins-bitbucket/images/tutorials/bitbucket/bitbucket-config.png) Bitbucket will be sending webhooks to Webhook Relay and our service will forwarding them to your internal Jenkins instance. ## [Configure Jenkins](#configure-jenkins) Ensure you have the Bitbucket Jenkins plugin. Plugin instructions can be found here: [https://plugins.jenkins.io/bitbucket/#plugin-content-bitbucket-cloud-usage](https://plugins.jenkins.io/bitbucket/#plugin-content-bitbucket-cloud-usage). In your repository configure the build trigger: ![configuring build trigger](https://webhookrelay.com/docs/tutorials/cicd/jenkins-bitbucket/images/tutorials/bitbucket/bitbucket-build.png) ## [Complete Step-by-Step Integration Guide](#complete-step-by-step-integration-guide) Follow these steps to integrate Bitbucket webhooks with Jenkins: 1. **Install Jenkins Bitbucket Plugin** - Navigate to Jenkins plugin manager and install the "Bitbucket" plugin 2. **Create Webhook Relay Account** - Sign up at Webhook Relay and create authentication tokens 3. **Set Up Internal Forwarding** - Configure Webhook Relay to forward to your Jenkins endpoint at `http://localhost:8080/bitbucket-hook/` 4. **Start Relay Agent** - Run the relay agent using your authentication credentials to begin forwarding 5. **Get Public URL** - Copy the public webhook URL provided by Webhook Relay 6. **Configure Bitbucket Webhook** - In your Bitbucket repository settings, add a new webhook with the public URL 7. **Configure Jenkins Job** - Enable "Build when a change is pushed to Bitbucket" trigger in your Jenkins job 8. **Test Integration** - Push a commit to Bitbucket and verify Jenkins triggers the build automatically ### [Configuration Code Example](#configuration-code-example) Start the Webhook Relay agent: ``` relay login -k your-token-key -s your-token-secret relay forward -b your-bucket-name http://localhost:8080/bitbucket-hook/ ``` ## [Testing](#testing) Once the agent is running, you can test by pushing a commit to your repository. You should see a build being triggered in Jenkins: ![push triggers the build](https://webhookrelay.com/docs/tutorials/cicd/jenkins-bitbucket/images/tutorials/bitbucket/jenkins-build.png) You should also see it in the terminal where you started Webhook Relay agent: ``` relay forward -b localhost-9Jk06s Filtering on bucket: localhost-9Jk06s Starting webhook relay agent... 2023-09-24 23:03:10.884 INFO using standard transport... 2023-09-24 23:03:10.951 INFO webhook relay ready... {"host": "my.webhookrelay.com:8080", "buckets": ["localhost-9Jk06s"]} ``` ### [Troubleshooting](#troubleshooting) There are several places to look for logs: - Webhook Relay bucket details. It will show all your webhooks and their requests, responses. - Jenkins system logs. You can find them in the Jenkins UI under "Manage Jenkins" -> "System Log". - Every time you commit to your repository, you should see Bitbucket webhooks in Webhook Relay bucket logs. ## [Frequently Asked Questions](#frequently-asked-questions) ### [Why is Jenkins not triggering builds when I push to Bitbucket?](#why-is-jenkins-not-triggering-builds-when-i-push-to-bitbucket) Check that you have enabled the "Build when a change is pushed to Bitbucket" trigger in your Jenkins job configuration. Also verify that the Webhook Relay agent is running and that Bitbucket webhooks are being received (check Webhook Relay logs). ### [Response "200" in Webhook Relay logs but no build](#response-200-in-webhook-relay-logs-but-no-build) It's possible that you don't have the SCM configuration matching your Bitbucket repository. Check the system logs in Jenkins for errors such as: ``` PM WARNING com.cloudbees.jenkins.plugins.BitbucketJobProbe triggerMatchingJobs No SCM configuration was found! ``` If you find them, add your repository to the SCM configuration in the Jenkins job. ### [On Webhook Relay all logs appear as "received"](#on-webhook-relay-all-logs-appear-as-received) You need to start the agent, follow the instructions [here](https://webhookrelay.com/docs/tutorials/cicd/jenkins-bitbucket/docs/installation/cli/). Agent is required to run in order to receive and forward webhooks. ### [How do I secure my Bitbucket webhook integration?](#how-do-i-secure-my-bitbucket-webhook-integration) Webhook Relay adds security by keeping Jenkins behind a firewall. Webhooks are uni-directional, so responses aren't returned to the caller. For additional security, you can use token-based authentication in Webhook Relay. ### [Can I use this setup with Bitbucket Server?](#can-i-use-this-setup-with-bitbucket-server) Yes! Bitbucket server webhook to jenkins example can be found here [https://plugins.jenkins.io/bitbucket/#plugin-content-bitbucket-server-usage](https://plugins.jenkins.io/bitbucket/#plugin-content-bitbucket-server-usage). You mostly need to install a webhook plugin [https://marketplace.atlassian.com/apps/1215474/post-webhooks-for-bitbucket?hosting=server&tab=overview](https://marketplace.atlassian.com/apps/1215474/post-webhooks-for-bitbucket?hosting=server&tab=overview) and then create a Post-WebHook, which is different from WebHook and enable on push. ### [What if my Jenkins is running on a different port?](#what-if-my-jenkins-is-running-on-a-different-port) Simply adjust the forwarding URL when setting up Webhook Relay. For example, if Jenkins is on port 9090, use `http://localhost:9090/bitbucket-hook/` instead of port 8080. Did this page help you? --- --- title: An expose.dev Alternative for Webhooks and Tunnels (2026) meta: "og: title": "An expose.dev Alternative for Webhooks and Tunnels (2026)" description: Compare Expose (expose.dev) and Webhook Relay for tunneling localhost and forwarding webhooks — stable URLs, transforms, fan-out, retries, free plan. url: https://webhookrelay.com/blog/expose-dev-alternative.md file: /blog/expose-dev-alternative.md --- ![Stripes](https://webhookrelay.com/blog/expose-dev-alternative/images/stripes.svg) # **An expose.dev Alternative for Webhooks and Tunnels (2026)** Compare Expose (expose.dev) and Webhook Relay for tunneling localhost and forwarding webhooks — stable URLs, transforms, fan-out, retries, free plan. ![An expose.dev Alternative for Webhooks and Tunnels (2026)](https://webhookrelay.com/blog/expose-dev-alternative/images/blog/heroes/alternative.jpg) If you searched for an **expose.dev alternative**, you're probably weighing [Expose](https://expose.dev/) — BeyondCode's elegant, open-source tunnel written in pure PHP — against tools built specifically for webhooks. Expose is excellent at what it does: it shares your local site over a public subdomain, ships a clean request dashboard, and is self-hostable. But as of 2026 (verify current details), Expose is fundamentally a **general-purpose tunnel**, not webhook infrastructure. Webhook Relay approaches the same problem from the webhook side: a stable public URL, request inspection, retries, and the ability to forward, transform and fan-out webhooks to localhost or any private server. ## [TL;DR](#tldr) - **Sharing a Laravel/PHP local site or want a self-hosted tunnel?** Expose is a great fit, especially with its Herd integration. - **The job is webhooks — transform, fan-out, retry, forward to private infra?** That's where Webhook Relay is built to live. - **Want to inspect provider webhooks with zero install?** Use the free [Webhook Bin](https://webhookrelay.com/blog/expose-dev-alternative/webhook-bin/) in your browser, then forward to localhost with the agent. - **Need the webhook to reach a private server, container, or Kubernetes pod?** Webhook Relay forwards into private networks with no public IP. ## [Expose vs Webhook Relay at a glance](#expose-vs-webhook-relay-at-a-glance) | | Expose (expose.dev) | Webhook Relay | | --- | --- | --- | | Primary purpose | General tunnel for local sites | Webhook forwarding + tunnels | | Open source / self-hostable | Yes | Hosted service | | Stable URL | Reserved subdomains on Pro | Yes, every plan | | Inspect requests | Yes (request dashboard) | Yes ([Webhook Bin](https://webhookrelay.com/blog/expose-dev-alternative/webhook-bin/)) | | Replay requests | Yes | Yes | | Forward to localhost | Yes (it's a tunnel) | Yes (via the [relay agent](https://webhookrelay.com/blog/expose-dev-alternative/tunnels/)) | | Forward to a private network / Kubernetes | No (just the tunnel host) | Yes | | Transform payloads (JS/Lua) | No | [Yes](https://webhookrelay.com/blog/expose-dev-alternative/features/transform-webhooks/) | | Fan-out to multiple destinations | No | [Yes](https://webhookrelay.com/blog/expose-dev-alternative/features/webhook-multiple-destinations/) | | Retries on failure | No | Yes | | Laravel / Herd integration | Yes | No | | Free plan | Free tier + paid Pro | Yes, free plan; paid from $9.99/mo | _Competitor details reflect publicly documented plans as of 2026 and can change — verify Expose's current features and pricing before deciding._ ## [Where Expose shines](#where-expose-shines) Let's be fair. Expose is a well-built tool with a clear audience: - **PHP and Laravel ergonomics.** Written in pure PHP, it fits naturally into Laravel projects and integrates with Herd for one-command sharing. - **Self-hostable.** Because it's fully open source, you can run the whole server yourself for privacy, custom domains, and no connection limits. - **A polished request dashboard.** Expose lets you watch incoming HTTP requests in real time and replay (and even modify) them — handy for debugging. If you're a PHP/Laravel developer who wants a self-hostable tunnel for sharing local sites, Expose is a strong default. ## [Where Webhook Relay wins for webhooks](#where-webhook-relay-wins-for-webhooks) ### [1. Webhook semantics, not just a tunnel](#_1-webhook-semantics-not-just-a-tunnel) A tunnel — including Expose — moves bytes between a public URL and your local host. Webhook Relay **routes a webhook** and can act on it. Because it sits in the path, you can [transform payloads](https://webhookrelay.com/blog/expose-dev-alternative/features/transform-webhooks/) with JavaScript or Lua (turn a raw GitHub event into a Slack message), [fan-out to multiple destinations](https://webhookrelay.com/blog/expose-dev-alternative/features/webhook-multiple-destinations/), filter noisy events, add authentication, or retry automatically when a delivery fails. Those are webhook-infrastructure features a general tunnel doesn't aim to provide. ### [2. Forward to localhost _and_ private networks](#_2-forward-to-localhost-and-private-networks) Expose exposes the machine running the client. Webhook Relay forwards a webhook to wherever it needs to go — your laptop on `localhost:8080`, an internal API behind a firewall, or a Kubernetes service with no public IP: ``` # Install the agent, then forward your public endpoint to a local port relay forward --bucket my-app http://localhost:8080/webhook ``` The agent makes an **outbound** connection, so there are no firewall ports to open. When you do need a general-purpose tunnel, Webhook Relay offers [tunnels](https://webhookrelay.com/blog/expose-dev-alternative/tunnels/) too. ### [3. A stable URL on every plan](#_3-a-stable-url-on-every-plan) With Webhook Relay, your endpoint URL is fixed on every plan, including free — set it once in Stripe or GitHub and forget it. With Expose, reserved subdomains are a Pro feature (verify current details), so a stable URL there typically means paying or self-hosting. ### [4. Inspect webhooks with zero install](#_4-inspect-webhooks-with-zero-install) Open [Webhook Bin](https://webhookrelay.com/blog/expose-dev-alternative/webhook-bin/), get an instant URL, and watch requests arrive in real time — no signup, no PHP runtime, no install. When you're ready to forward them somewhere, [create a free account](https://my.webhookrelay.com/register) and install the agent. ### [5. Cross-stack, hosted, and maintained](#_5-cross-stack-hosted-and-maintained) Expose is a great fit if you live in PHP. Webhook Relay is language-agnostic hosted infrastructure — the agent runs anywhere (CLI, Docker, Kubernetes), and there's nothing to operate or keep patched yourself unless you want to. ## [How to switch from Expose for webhooks](#how-to-switch-from-expose-for-webhooks) 1. **Inspect first (no install):** open [Webhook Bin](https://webhookrelay.com/blog/expose-dev-alternative/webhook-bin/), copy the URL, and point your provider at it. 2. **Forward to localhost:** [create a free account](https://my.webhookrelay.com/register), install the agent, and run `relay forward`. 3. **Add webhook logic:** [transform](https://webhookrelay.com/blog/expose-dev-alternative/features/transform-webhooks/), [fan-out](https://webhookrelay.com/blog/expose-dev-alternative/features/webhook-multiple-destinations/), or retry as needed — no extra services. ## [When to pick which](#when-to-pick-which) - **Pick Expose** if you're a PHP/Laravel developer who wants a self-hostable tunnel for sharing local sites, value the Herd integration, and don't need webhook-specific features. - **Pick Webhook Relay** when the work is webhooks: stable URLs on any plan, forwarding into private infrastructure, transforming and fanning-out events, retries on failure, and a hosted service you don't have to run. Ready to forward webhooks the easy way? [Start for free](https://my.webhookrelay.com/register) or [test a webhook now](https://webhookrelay.com/blog/expose-dev-alternative/webhook-bin/). ![Stripes](https://webhookrelay.com/blog/expose-dev-alternative/images/stripes-dark.svg) ## **Start forwarding webhooks in minutes ** Connect a source, pick a destination, and Webhook Relay handles delivery, retries and transforms. Set up your first webhook in under five minutes. [Start for free ->](https://my.webhookrelay.com/register) Free plan · No credit card required · 7-day money-back guarantee on paid plans --- --- title: Jenkins and GitHub | WebhookRelay meta: "og: title": "Jenkins and GitHub" description: Configuring Jenkins CI to receive webhooks from Github without public IP/NAT or behind a firewall url: https://webhookrelay.com/docs/tutorials/cicd/jenkins-github.md file: /docs/tutorials/cicd/jenkins-github.md --- ![Stripes](https://webhookrelay.com/docs/tutorials/cicd/jenkins-github/images/stripes.svg) Documentation **Fundamentals** # **Jenkins and GitHub** Configuring Jenkins CI to receive webhooks from Github without public IP/NAT or behind a firewall In this tutorial, we will configure Jenkins Blue Ocean to instantly receive webhooks from GitHub.com behind a firewall and without public IP/domain (which could be a corporate firewall, a network behind a NAT/CGNAT like you have at home). You can generalize this to other services too - such as BitBucket, GitLab, DockerHub, or anything that emits webhooks. Main advantages of webhooks over polling are: - No delay between polling requests - Your Jenkins CI server doesn't risk getting rate-limited by the GitHub API (exhausting API quota) ![Github to Jenkins without public IP](https://webhookrelay.com/docs/tutorials/cicd/jenkins-github/images/tutorials/jenkins/github-webhooks-jenkins.png) And the main advantages of Webhook Relay here are: - A single agent can handle hundreds of Jenkins servers in your internal network. - Any internal service can benefit from receiving webhooks without exposing it directly to the internet. - Additional security layer as Jenkins is not exposed to the internet as webhooks by default are uni-directional, responses are not returned to the sender. - Public endpoints can stay the same, but servers underneath can change (if you want to provision a new machine, a configuration can stay the same). > Support request: if you find any issues with the PR, please inform us at [](https://webhookrelay.com/docs/tutorials/cicd/jenkins-github//mailto:support@webhookrelay.com?Subject=Tutorial%20issue) [support@webhookrelay.com](https://webhookrelay.com/docs/tutorials/cicd/jenkins-github//mailto:support@webhookrelay.com). ## [Prerequisites](#prerequisites) - Webhook Relay account, create one [here](https://my.webhookrelay.com). - An Ubuntu VM. Other OS types should also be fine. - Jenkins instance. We will not go into installing Jenkins itself as there are quite a few options and many articles on that. See Jenkins [official docs](https://www.jenkins.io/doc/book/installing/) for up-to-date instructions. - [GitHub](https://github.com) account and a repository that you will want to use. ## [Configuration](#configuration) We will install an agent that will subscribe to webhooks and relay them to your Jenkins instance. An agent can be on the same machine or any other as long as it has network access to your Jenkins instance. A common setup is where one or several VMs have agents installed and are relaying webhooks to multiple machines inside your internal cluster. ### [Step 1: Install and authenticate CLI](#step-1-install-and-authenticate-cli) Installation instructions for CLI can be found in the [official CLI installation page](https://docs.webhookrelay.com/installation-options/installation-options/install-cli). This tutorial is focusing on Linux x86-64 machines which is the most common type: ``` sudo wget -O \ /usr/local/bin/relay \ https://storage.googleapis.com/webhookrelay/downloads/relay-linux-amd64 ``` Give it permissions to execute and update itself: ``` sudo chmod +wx /usr/local/bin/relay ``` Once the agent is downloaded, log in using the token key & secret pair generated from the [tokens page](https://my.webhookrelay.com/tokens). Just click on the "**CREATE TOKEN**" button and either set environment variables in your shell or use `relay login ...` command. ### [Step 2: Configure routing & GitHub](#step-2-configure-routing-github) Webhook Relay CLI has a shorthand command which can create the configuration and start forwarding webhooks, however since we are configuring for production, we will just create configuration but ``` relay forward --bucket github-jenkins http://localhost:8080/github-webhook/ --no-agent ``` It should display your public endpoint: ``` ... Forwarding configuration created: https://vlndyzsibcil98gdte7yp1.hooks.webhookrelay.com -> http://localhost:8080/github-webhook/ ``` Take that *.hooks.webhookrelay.com URL and put it to your Github repository webhooks section. You can find it by going into **Settings** -> **Webhooks**. Add a new webhook configuration: - Your unique generated endpoint (paid plans can choose subdomain name or use their controlled domains) - Content type set to 'application/json' - Create a shared secret (you can use a password manager or just type `openssl rand -base64 32` in your terminal) - 'Send me everything' ![Github webhook configuration](https://webhookrelay.com/docs/tutorials/cicd/jenkins-github/images/tutorials/jenkins/github-webhooks-configure.png) You can also always use your public endpoints and destinations in the [buckets page](https://my.webhookrelay.com/buckets). > In general it's a good practice to set the shared secret. ### [Step 3: Install agent as a service on your machine](#step-3-install-agent-as-a-service-on-your-machine) We want the agent to reconnect if your machine restarts (after a power outage or more commonly after an update). Therefore we have two options: - **Docker** - simple, when Docker is available in your system (recommended solution). - **Background service** - a simple and powerful solution where agent installs itself as an OS background service. ### [Option 1: Installing agent as a Docker container](#option-1-installing-agent-as-a-docker-container) In this case, we will let Docker always start & restart the Webhook Relay agent container. You can use the same access token key & secret that you have generated previously, otherwise, create a new pair [here](https://my.webhookrelay.com/tokens) and export them as an environment variables: ``` export RELAY_KEY=XXXX export RELAY_SECRET=YYYY export BUCKETS=github-jenkins ``` Start the agent container: ``` docker run -d --restart always \ --name webhookrelayd \ --network host \ --env RELAY_KEY=$RELAY_KEY \ --env BUCKETS=$BUCKETS \ --env RELAY_SECRET=$RELAY_SECRET webhookrelay/webhookrelayd ``` To view the logs: ``` docker logs webhookrelayd 2020-06-25 21:36:24.354 INFO using standard transport... 2020-06-25 21:36:24.474 INFO webhook relay ready... {"host": "my.webhookrelay.com:8080", "buckets": ["github-jenkins"]} ``` ### [Option 2: Installing the agent as a background service](#option-2-installing-the-agent-as-a-background-service) Additional documentation for the background service (logging, proxy, upgrade, removal) can be viewed [here](https://docs.webhookrelay.com/installation-options/installation-options/background-service). We will just create a configuration file: ``` version: "v1" key: XXXX # will be encrypted on startup secret: YYYY # will be encrypted on startup buckets: - github-jenkins ``` > Upon startup, key and secret will be encrypted. Now, in your machine let's create a directory where we can put this configuration: ``` mkdir -p /opt/config/webhookrelay ``` You can use `vim` or any other text editor to open it and copy & paste the config: ``` vim /opt/config/webhookrelay/relay.yaml ``` Let's start the agent: ``` relay service install -c /opt/config/webhookrelay/relay.yaml relay service start ``` ### [Step 4: Configure Jenkins webhook shared secret](#step-4-configure-jenkins-webhook-shared-secret) The secret from step 2 has to be added to Jenkins CI for it to recognize the webhooks. Go to Jenkins configuration and scroll down until you reach the GitHub section. Once there, click on "Advanced" and click "Add" next to _Shared secrets_. Set your secret here. ![Jenkins shared secret](https://webhookrelay.com/docs/tutorials/cicd/jenkins-github/images/tutorials/jenkins/jenkins-shared-secret.png) ### [Step 5: Configuring Jenkins pipeline](#step-5-configuring-jenkins-pipeline) Click on create a new Jenkins CI CD pipeline, select "GitHub" as your code store: ![Repository configuration](https://webhookrelay.com/docs/tutorials/cicd/jenkins-github/images/tutorials/jenkins/jenkins-repo-config.png) Add access token and select repository. That's it, if you push to the repository, you should see pipeline run: ![Jenkins pipeline run](https://webhookrelay.com/docs/tutorials/cicd/jenkins-github/images/tutorials/jenkins/jenkins-pipeline-run.png) ## [Troubleshooting](#troubleshooting) ### [400 Bad Request from Jenkins](#_400-bad-request-from-jenkins) If you open Webhook Relay [logs dashboard](https://my.webhookrelay.com/logs) you should see detailed logs. If they say: ``` Error 400 Signature was expected, but not provided

HTTP ERROR 400 Signature was expected, but not provided

URI:/github-webhook/
STATUS:400
MESSAGE:Signature was expected, but not provided
SERVLET:Stapler

Powered by Jetty:// 9.4.27.v20200227
``` Then the secret in GitHub either is not set or doesn't match. ### [403 No valid crumb was included in the request](#_403-no-valid-crumb-was-included-in-the-request) This happens when the request is sent to a Jenkins API path that is not whitelisted for webhooks. It could be that GitHub is sending a request with some additional path (like /ghprb or anything else) which gets automatically amended to your destination resulting in a wrong final path. To fix this, ensure that you are sending a request to a correct URL and that destination also doesn't expect anything more. ### [200 on Jenkins but nothing happens](#_200-on-jenkins-but-nothing-happens) In this case, it could be that you haven't selected the correct repository when setting up a pipeline. When the webhook is received, based on received data Jenkins finds a repository to poll for changes. If repository from the webhook doesn't match repository in the pipeline - it won't do anything. Did this page help you? --- --- title: Jira Webhooks: Setup, Payload and Security | WebhookRelay meta: "og: title": "Jira Webhooks: Setup, Payload and Security" description: How to set up Jira webhooks — admin UI vs REST API, JQL filters, the payload format, X-Hub-Signature verification and testing. A complete, practical guide. url: https://webhookrelay.com/blog/jira-webhooks-guide.md file: /blog/jira-webhooks-guide.md --- ![Stripes](https://webhookrelay.com/blog/jira-webhooks-guide/images/stripes.svg) # **Jira Webhooks: Setup, Payload and Security** How to set up Jira webhooks — admin UI vs REST API, JQL filters, the payload format, X-Hub-Signature verification and testing. A complete, practical guide. ![Jira webhooks guide](https://webhookrelay.com/blog/jira-webhooks-guide/images/blog/heroes/route.jpg) Jira can notify your code the moment an issue is created, updated, transitioned or commented on. That is what **Jira webhooks** are: HTTP POSTs that Jira Cloud (or Data Center) sends to a URL you choose, carrying the issue as JSON. This guide covers the parts people actually get stuck on — the two ways to register a webhook, JQL filtering, what the payload really contains, and how to secure and test the whole thing. If your immediate goal is receiving Jira events on `localhost`, we have a focused walkthrough for that: [Test Jira webhooks locally](https://webhookrelay.com/blog/jira-webhooks-guide/blog/receive-jira-webhooks-locally/). This guide is the broader reference. ## [Two ways to register a Jira webhook](#two-ways-to-register-a-jira-webhook) **1. The admin UI (classic webhooks).** Go to **Settings → System → Webhooks → Create a WebHook**. You give it a name, a URL, select events (issue created/updated/deleted, comment events, project/version/user events, and more) and optionally a JQL filter. This is the fastest path and fine for internal tooling — but these webhooks are **not signed**, and they are instance-wide, so you need Jira admin rights. **2. The REST API (dynamic webhooks).** Apps using OAuth 2.0 or Connect register webhooks with `POST /rest/api/3/webhook`. Dynamic webhooks support a **signing secret** (verification below), are scoped to the app that created them, and expire — Jira Cloud requires apps to refresh them periodically (they last 30 days unless extended), so schedule a refresh job. There is a third, often-overlooked option: **Automation rules**. Jira's automation can fire a "Send web request" action on any rule trigger, which behaves like a webhook with full control over method, headers and body. If you need custom headers (say, an `Authorization` token) this is the easiest way to get them — classic webhooks send no custom headers. ## [Scope deliveries with JQL](#scope-deliveries-with-jql) A webhook without a filter receives events for the whole instance. Add a JQL filter at registration time to narrow it: ``` project = PROJ AND issuetype = Bug ``` Only events for matching issues are delivered. This is the single best lever for keeping your handler simple — filter in Jira, not in code. ## [The payload](#the-payload) Every delivery is JSON with a `webhookEvent` field. An issue update looks like this (trimmed): ``` { "timestamp": 1768471800000, "webhookEvent": "jira:issue_updated", "user": { "accountId": "5f00...aa", "displayName": "Alex Doe" }, "issue": { "key": "PROJ-42", "fields": { "summary": "Checkout button unresponsive on mobile", "status": { "name": "In Progress" }, "priority": { "name": "High" }, "assignee": { "displayName": "Alex Doe" } } }, "changelog": { "items": [ { "field": "status", "fromString": "To Do", "toString": "In Progress" } ] } } ``` Things worth knowing: - **`changelog` is where the diff lives.** For `jira:issue_updated` it lists each changed field with `fromString`/`toString`. If you only care about status transitions, read the changelog instead of diffing the whole issue. - **Comment events** carry a `comment` object alongside the issue. - **Deliveries include tracing headers** — `X-Atlassian-Webhook-Identifier` uniquely identifies the delivery attempt, which is handy for idempotency and log correlation. - Payloads can be large (full issue with all custom fields). Budget for tens of kilobytes. Want to see the real thing without writing a handler? Open a free [Webhook Bin](https://webhookrelay.com/blog/jira-webhooks-guide/webhook-bin/), point a test webhook at it and trigger an event — or use the bin's sample catalog, which includes real-shaped `jira:issue_updated` and `jira:issue_created` events you can send to any endpoint with one click. ## [Securing Jira webhooks](#securing-jira-webhooks) How you secure a Jira webhook depends on how it was created: - **Dynamic webhooks (REST API) with a secret** are signed: Jira computes HMAC-SHA256 over the raw body and sends it as `sha256=…` in the **`X-Hub-Signature`** header. Recompute it with your secret and compare in constant time. You can sanity-check an implementation with the free [HMAC signature verifier](https://webhookrelay.com/blog/jira-webhooks-guide/hmac-verification/), and the general pitfalls (raw body, timing-safe compare) are covered in [Verify a webhook signature](https://webhookrelay.com/blog/jira-webhooks-guide/blog/verify-webhook-signature/). - **Classic webhooks (admin UI) are not signed.** Mitigate with a secret token in the URL path or query string (`/hooks/jira?token=…`) that your handler checks, and/or restrict inbound traffic to [Atlassian's published IP ranges](https://support.atlassian.com/organization-administration/docs/ip-addresses-and-domains-for-atlassian-cloud-products/). - **Automation "Send web request"** lets you set your own auth header — treat it like any API client. ## [Delivery behavior and reliability](#delivery-behavior-and-reliability) Jira Cloud expects your endpoint to answer quickly with a 2xx. Slow or failing endpoints get their deliveries dropped — Jira's retry behavior is limited, so **do not treat webhook delivery as guaranteed**. The standard hardening pattern: 1. Return `200` immediately, queue the event, process asynchronously. 2. Deduplicate on `X-Atlassian-Webhook-Identifier` (retried attempts reuse context). 3. Reconcile periodically against the REST API for anything missed. If you need stronger guarantees than Jira gives you, put a stable relay endpoint in front of your handler: Webhook Relay stores every delivery and [retries towards your destination for up to 30 days](https://webhookrelay.com/blog/jira-webhooks-guide/features/durable-retries/), so a redeploy or an outage on your side no longer loses events. ## [Testing your integration](#testing-your-integration) The fast loop: 1. **Inspect first** — point the webhook at a [Webhook Bin](https://webhookrelay.com/blog/jira-webhooks-guide/webhook-bin/) and trigger real events to see exact headers and bodies. 2. **Develop locally** — forward events to your machine with `relay forward --bucket jira http://localhost:8080/webhook`; the [local testing guide](https://webhookrelay.com/blog/jira-webhooks-guide/blog/receive-jira-webhooks-locally/) walks through it. 3. **Replay** captured deliveries against your handler while you iterate, instead of re-triggering issues in Jira. ## [Related reading](#related-reading) - [Test Jira webhooks locally](https://webhookrelay.com/blog/jira-webhooks-guide/blog/receive-jira-webhooks-locally/) — the localhost-focused walkthrough - [Send webhooks _to_ Jira](https://webhookrelay.com/blog/jira-webhooks-guide/blog/webhook-to-jira/) — the reverse direction: creating issues from incoming webhooks - [Confluence webhooks](https://webhookrelay.com/blog/jira-webhooks-guide/blog/confluence-webhooks/) — the same patterns on Jira's sibling - [How to test webhooks](https://webhookrelay.com/blog/jira-webhooks-guide/blog/how-to-test-webhooks/) and [What is a webhook](https://webhookrelay.com/blog/jira-webhooks-guide/blog/what-is-webhook/) --- --- title: WhatsApp Cloud API Webhooks: Setup & Tokens | WebhookRelay meta: "og: title": "WhatsApp Cloud API Webhooks: Setup & Tokens" description: Set up WhatsApp Cloud API webhooks: the callback URL, the hub.challenge verify token handshake, payload examples and X-Hub-Signature-256 verification. url: https://webhookrelay.com/blog/whatsapp-cloud-api-webhooks.md file: /blog/whatsapp-cloud-api-webhooks.md --- ![Stripes](https://webhookrelay.com/blog/whatsapp-cloud-api-webhooks/images/stripes.svg) # **WhatsApp Cloud API Webhooks: Setup, Verify Token and Payloads** Set up WhatsApp Cloud API webhooks: the callback URL, the hub.challenge verify token handshake, payload examples and X-Hub-Signature-256 verification. ![WhatsApp Cloud API webhooks](https://webhookrelay.com/blog/whatsapp-cloud-api-webhooks/images/blog/heroes/route.jpg) Every incoming WhatsApp message, status update and delivery receipt reaches your code the same way: a **webhook from the WhatsApp Cloud API**. Setting one up trips people in a specific spot — the **verify token handshake** — and then again when the payloads arrive nested four levels deep. This guide walks the whole path: callback URL, verification, payload shapes, signatures, and a sane local dev loop. ## [How Cloud API webhooks work](#how-cloud-api-webhooks-work) You configure a single **callback URL** for your Meta app. Meta first _verifies_ it (a GET handshake), then delivers events (POSTs) for every **webhook field** you subscribe to — for WhatsApp, the one you almost always want is `messages`. Requirements: public **HTTPS** URL with a valid certificate, answering within a few seconds. There is no way to point Meta directly at `localhost` — see the last section for the clean workaround. ## [Step 1: The verify token handshake](#step-1-the-verify-token-handshake) In the [Meta App Dashboard](https://developers.facebook.com/apps/) open your app → **WhatsApp → Configuration → Webhook**. You enter two things: the **callback URL** and a **verify token** — any string you choose. When you hit save, Meta sends: ``` GET /your/webhook?hub.mode=subscribe&hub.verify_token=YOUR_TOKEN&hub.challenge=1158201444 ``` Your endpoint must check the token and echo the challenge: ``` // Express app.get('/webhook', (req, res) => { const { 'hub.mode': mode, 'hub.verify_token': token, 'hub.challenge': challenge } = req.query; if (mode === 'subscribe' && token === process.env.VERIFY_TOKEN) { return res.status(200).send(challenge); // raw value, no JSON } res.sendStatus(403); }); ``` The two classic mistakes: responding with JSON (`{"hub.challenge": …}` fails — Meta wants the raw value) and confusing the verify token with the app secret (they are unrelated; the token is only used in this handshake). After verification succeeds, click **Manage** and subscribe to the `messages` field — without this second step you stay silent. ## [Step 2: Read the payload](#step-2-read-the-payload) Deliveries are POSTs with a deeply nested envelope. An inbound text message: ``` { "object": "whatsapp_business_account", "entry": [{ "id": "WABA_ID", "changes": [{ "field": "messages", "value": { "messaging_product": "whatsapp", "metadata": { "display_phone_number": "15550001111", "phone_number_id": "PHONE_ID" }, "contacts": [{ "profile": { "name": "Alex Doe" }, "wa_id": "15557770000" }], "messages": [{ "from": "15557770000", "id": "wamid.HBgL...", "timestamp": "1768472000", "type": "text", "text": { "body": "hello!" } }] } }] }] } ``` Status updates (sent/delivered/read) arrive on the same URL with a `statuses` array instead of `messages`. Your handler should switch on what is present — and remember `entry` and `changes` are arrays; batches happen. The fastest way to learn the shapes is to look at real traffic: point the callback URL at a free [Webhook Bin](https://webhookrelay.com/blog/whatsapp-cloud-api-webhooks/webhook-bin/) for ten minutes of testing (it answers the GET handshake's `hub.challenge` echo requirement if you configure the bin response, or verify against your real endpoint first and then switch) and message your test number. ## [Step 3: Verify the signature](#step-3-verify-the-signature) Every POST carries `X-Hub-Signature-256: sha256=` — HMAC-SHA256 of the **raw body** keyed with your **app secret** (Dashboard → App settings → Basic). Recompute and compare in constant time; the mechanics and language snippets are identical to GitHub's scheme, covered in [Verify a webhook signature](https://webhookrelay.com/blog/whatsapp-cloud-api-webhooks/blog/verify-webhook-signature/), and you can sanity-check values in the free [HMAC verifier](https://webhookrelay.com/blog/whatsapp-cloud-api-webhooks/hmac-verification/). ## [Self-hosted gateways: WAHA, Evolution API and friends](#self-hosted-gateways-waha-evolution-api-and-friends) Not every WhatsApp integration uses Meta's hosted Cloud API. Self-hosted gateways — **WAHA**, **Evolution API**, **uazapi** and similar — run a WhatsApp Web session in a container and forward incoming messages to _your_ webhook URL with much simpler payloads (we see plenty of WAHA traffic in our own [webhook bin](https://webhookrelay.com/blog/whatsapp-cloud-api-webhooks/webhook-bin/)). The receiving patterns in this guide apply unchanged: inspect the gateway's payload in a bin first, then point it at your stable endpoint. No verify-token handshake, but also no Meta signature — check what auth headers your gateway can send and require them. ## [Local development without re-verifying](#local-development-without-re-verifying) Meta's URL verification makes tunnels painful: every time your tunnel URL changes, you re-run the handshake. The fix is a **stable** public endpoint that never changes, relayed to wherever your code currently runs: 1. Create a Webhook Relay bucket — its input URL is permanent. 2. Verify that URL with Meta **once**. 3. Forward to your machine: `relay forward --bucket whatsapp http://localhost:3000/webhook`. Your laptop connects outbound, so no ports open, and restarting or moving your dev environment never touches the Meta configuration. The full walkthrough is in [Test WhatsApp webhooks locally](https://webhookrelay.com/blog/whatsapp-cloud-api-webhooks/blog/receive-whatsapp-webhooks-locally/). Building the automation in n8n instead of code? See the [n8n WhatsApp Cloud API webhook tutorial](https://webhookrelay.com/blog/whatsapp-cloud-api-webhooks/docs/tutorials/n8n/whatsapp-cloud-api-webhook/). ## [Related reading](#related-reading) - [Test WhatsApp webhooks locally](https://webhookrelay.com/blog/whatsapp-cloud-api-webhooks/blog/receive-whatsapp-webhooks-locally/) - [n8n WhatsApp Cloud API webhook trigger](https://webhookrelay.com/blog/whatsapp-cloud-api-webhooks/docs/tutorials/n8n/whatsapp-cloud-api-webhook/) - [Verify a webhook signature](https://webhookrelay.com/blog/whatsapp-cloud-api-webhooks/blog/verify-webhook-signature/) - [How to test webhooks](https://webhookrelay.com/blog/whatsapp-cloud-api-webhooks/blog/how-to-test-webhooks/) --- --- title: DocuSign Connect Webhooks: Events, HMAC and Retry Behavior meta: "og: title": "DocuSign Connect Webhooks: Events, HMAC and Retry Behavior" description: A practical guide to DocuSign Connect webhooks: envelope and recipient events, the JSON payload, HMAC verification, and Connect's retry behavior. url: https://webhookrelay.com/blog/docusign-connect-webhooks.md file: /blog/docusign-connect-webhooks.md --- ![Stripes](https://webhookrelay.com/blog/docusign-connect-webhooks/images/stripes.svg) # **DocuSign Connect Webhooks: Events, HMAC and Retry Behavior** A practical guide to DocuSign Connect webhooks: envelope and recipient events, the JSON payload, HMAC verification, and Connect's retry behavior. ![DocuSign Connect webhooks](https://webhookrelay.com/blog/docusign-connect-webhooks/images/blog/heroes/route.jpg) When an envelope is signed, declined or completed, **DocuSign Connect** is the machinery that tells your application — a webhook POST with the envelope state as JSON. Connect is more configurable than most providers' webhooks (account-wide vs per-envelope, event filtering, includes), and it has one behavior that surprises everyone the first time: **it retries hard**. We run a public [webhook bin](https://webhookrelay.com/blog/docusign-connect-webhooks/webhook-bin/) and routinely watch DocuSign hitting long-dead endpoints with `retryCount` in the twenties. This guide covers setup, events, payloads, HMAC verification, and how to build a listener that survives all of that. ## [Two ways to get events](#two-ways-to-get-events) **Account-level Connect configuration.** In eSignature admin: **Settings → Connect → Add configuration**. You set the listener URL, pick events, and every matching envelope in the account triggers a delivery. This is the right choice for CRM syncs, archives, and anything ongoing. **Per-envelope `eventNotification`.** When creating an envelope via the API, attach an `eventNotification` object with your URL and the events you want — scoped to that envelope only. Right for multi-tenant apps where each envelope may notify a different customer system. Both deliver the same JSON format (use the modern **JSON (REST v2.1)** format; the legacy XML format still exists in old configurations). ## [The events](#the-events) The two families you will subscribe to: - **Envelope events**: `envelope-sent`, `envelope-delivered`, `envelope-completed`, `envelope-declined`, `envelope-voided` - **Recipient events**: `recipient-sent`, `recipient-delivered`, `recipient-completed`, `recipient-declined`, `recipient-authenticationfailed`, and more For a typical "do something when the document is fully signed" integration, `envelope-completed` alone is enough — recipient events are for tracking individual signers through the flow. ## [The payload](#the-payload) A JSON delivery looks like this (trimmed): ``` { "event": "envelope-completed", "apiVersion": "v2.1", "uri": "/restapi/v2.1/accounts/{accountId}/envelopes/{envelopeId}", "retryCount": 0, "configurationId": 10000001, "generatedDateTime": "2026-01-15T10:30:00.0000000Z", "data": { "accountId": "a1b2c3d4-…", "envelopeId": "e5f6a7b8-…", "envelopeSummary": { "status": "completed", "emailSubject": "Please sign: Services Agreement", "recipients": { "signers": [{ "name": "Alex Doe", "status": "completed" }] } } } } ``` Notes from real traffic: - **`retryCount` is your friend.** Zero means first attempt; anything higher means DocuSign already tried and failed. Use it together with `data.envelopeId` for idempotency. - Enable **include options** (documents, certificate of completion) only if you need them — payloads grow from a few KB to whole signed PDFs base64-encoded inline. - The [Webhook Bin sample catalog](https://webhookrelay.com/blog/docusign-connect-webhooks/webhook-bin/) includes real-shaped `recipient-delivered` and `envelope-completed` events you can POST at your handler with one click. ## [Verify the HMAC signature](#verify-the-hmac-signature) Connect deliveries are **unsigned by default** — you must enable HMAC on the configuration and store the generated key (DocuSign shows it once). After that, every delivery carries: ``` X-DocuSign-Signature-1: Base64(HMAC-SHA256(raw_body, key)) ``` Additional active keys arrive as `X-DocuSign-Signature-2`, `-3`, … (up to 100), which is how you rotate keys with zero downtime — verify against each active key and accept on any match. Test your implementation interactively with our free [DocuSign webhook signature verifier](https://webhookrelay.com/blog/docusign-connect-webhooks/verify-docusign-webhook-signature/), and read [Verify a webhook signature](https://webhookrelay.com/blog/docusign-connect-webhooks/blog/verify-webhook-signature/) for the general pitfalls (raw bytes, constant-time comparison). ## [Listener best practices — designed around the retries](#listener-best-practices-designed-around-the-retries) DocuSign's delivery contract: answer **2xx within 100 seconds** or the delivery counts as failed and enters the retry queue (roughly 24 hours of attempts at growing intervals). That, plus what we observe in the wild (20+ retries against dead endpoints), dictates the architecture: 1. **Acknowledge fast, process later.** Persist the raw delivery to a queue and return `200` immediately. Never do the PDF download or the CRM write inline. 2. **Be idempotent.** Key on `envelopeId` + `event` + status; retries and out-of-order deliveries both happen. 3. **Do not trust event ordering.** A `recipient-completed` retry can arrive after the `envelope-completed` that logically follows it. Treat each event as "fetch current state if in doubt" — the `uri` field gives you the exact API path. 4. **Keep the endpoint stable.** Every URL change re-enters the config UI. A stable relay endpoint in front (with [storage and up to 30 days of retries towards your side](https://webhookrelay.com/blog/docusign-connect-webhooks/features/durable-retries/)) decouples DocuSign's configuration from your infrastructure — deploys and outages on your end stop being delivery failures DocuSign has to retry. ## [Local development](#local-development) DocuSign only POSTs to public HTTPS URLs. For the dev loop — inspect the payload in a [Webhook Bin](https://webhookrelay.com/blog/docusign-connect-webhooks/webhook-bin/), then forward live events to `localhost` with the relay agent — follow [Test DocuSign webhooks locally](https://webhookrelay.com/blog/docusign-connect-webhooks/blog/receive-docusign-webhooks-locally/). ## [Related reading](#related-reading) - [Test DocuSign webhooks locally](https://webhookrelay.com/blog/docusign-connect-webhooks/blog/receive-docusign-webhooks-locally/) - [DocuSign webhook signature verifier](https://webhookrelay.com/blog/docusign-connect-webhooks/verify-docusign-webhook-signature/) — free, in-browser - [Verify a webhook signature](https://webhookrelay.com/blog/docusign-connect-webhooks/blog/verify-webhook-signature/) - [How to test webhooks](https://webhookrelay.com/blog/docusign-connect-webhooks/blog/how-to-test-webhooks/) --- --- title: Zendesk Webhooks: Setup, Signatures and Payload Examples meta: "og: title": "Zendesk Webhooks: Setup, Signatures and Payload Examples" description: Create Zendesk webhooks the right way: triggers vs event subscriptions, the payload format, verifying X-Zendesk-Webhook-Signature, and retries. url: https://webhookrelay.com/blog/zendesk-webhooks-guide.md file: /blog/zendesk-webhooks-guide.md --- ![Stripes](https://webhookrelay.com/blog/zendesk-webhooks-guide/images/stripes.svg) # **Zendesk Webhooks: Setup, Signatures and Payload Examples** Create Zendesk webhooks the right way: triggers vs event subscriptions, the payload format, verifying X-Zendesk-Webhook-Signature, and retries. ![Zendesk webhooks guide](https://webhookrelay.com/blog/zendesk-webhooks-guide/images/blog/heroes/route.jpg) Zendesk's webhooks moved on from the old "HTTP targets" years ago — today there is one **Webhooks** system in Admin Center that can be wired to business rules _or_ subscribe directly to typed events, with proper HMAC signatures on every delivery. This guide covers choosing between those two invocation styles, what the payloads look like, verifying the signature, and testing against real deliveries. ## [Create the webhook](#create-the-webhook) **Admin Center → Apps and integrations → Webhooks → Create webhook.** The first decision is how it gets invoked: - **Trigger or automation** — the webhook fires when a business rule matches (ticket created, priority changed to urgent, SLA about to breach…). The request body is whatever the rule author builds, usually JSON with placeholders. Great when a human designs the notification. - **Zendesk events** — the webhook subscribes to typed event streams like `zen:event-type:ticket.status_changed`, `ticket.priority_changed`, or user/organization events. The body is a **fixed envelope** Zendesk defines. This is what you want for programmatic consumers. Then set the endpoint URL (public HTTPS), request method, and optional authentication (basic auth, bearer token, or API key header) — this auth is _outbound_ config Zendesk attaches to requests, separate from the signature below. ## [The payload](#the-payload) Event-subscription deliveries share a stable envelope: ``` { "account_id": 10000001, "id": "01JEXAMPLEEVENT0000000000", "subject": "zen:ticket:15492", "time": "2026-01-15T10:30:00Z", "type": "zen:event-type:ticket.status_changed", "zendesk_event_version": "2022-11-06", "detail": { "id": "15492", "status": "open", "priority": "normal", "subject": "Cannot reset my password", "assignee_id": "400000000001" }, "event": { "previous": "new", "current": "open" } } ``` `type` names the event, `detail` carries the entity snapshot, and `event` describes the change itself (previous → current). Trigger-based webhooks look however the rule author shaped them — which is exactly why you should capture a real one before writing parsing code: point the webhook at a free [Webhook Bin](https://webhookrelay.com/blog/zendesk-webhooks-guide/webhook-bin/) for a minute (the bin's sample catalog also has a ready-made `ticket.status_changed` event with the full header set). ## [Verify the signature](#verify-the-signature) Every delivery — regardless of invocation style — carries: ``` X-Zendesk-Webhook-Signature: X-Zendesk-Webhook-Signature-Timestamp: 2026-01-15T10:30:00Z ``` The signed string is the **timestamp concatenated directly with the raw body** (no separator): ``` const expected = crypto.createHmac('sha256', secret) .update(timestamp + rawBody) .digest('base64'); ``` The signing secret lives on the webhook's detail page (**Reveal secret**) or via `GET /api/v2/webhooks/{id}/signing_secret`. Two details that cause most failures: hashing a re-serialized body instead of the raw bytes, and forgetting the timestamp prefix. Check your implementation interactively with our free [Zendesk webhook signature verifier](https://webhookrelay.com/blog/zendesk-webhooks-guide/verify-zendesk-webhook-signature/) — and reject deliveries whose timestamp is stale to block replays. ## [Delivery, retries and the activity log](#delivery-retries-and-the-activity-log) Zendesk expects a fast 2xx. Failed deliveries are retried a limited number of times with backoff, and webhooks that keep failing get **automatically deactivated** — a surprise you discover days later when tickets stop syncing. Defenses: 1. Acknowledge immediately, process asynchronously. 2. Watch the **activity log** on the webhook's admin page — it records each attempt, response code and latency. 3. If your consumer is flaky or occasionally deployed, front it with a stable relay endpoint that stores every event and [keeps retrying towards your side](https://webhookrelay.com/blog/zendesk-webhooks-guide/features/durable-retries/) — Zendesk always sees a healthy 200 and never deactivates the webhook. ## [Local development](#local-development) Zendesk needs a public URL, your handler runs on `localhost` — the standard bridge applies: inspect in a [Webhook Bin](https://webhookrelay.com/blog/zendesk-webhooks-guide/webhook-bin/), then `relay forward --bucket zendesk http://localhost:8080/webhook`. Full walkthrough: [Test Zendesk webhooks locally](https://webhookrelay.com/blog/zendesk-webhooks-guide/blog/receive-zendesk-webhooks-locally/). ## [Related reading](#related-reading) - [Test Zendesk webhooks locally](https://webhookrelay.com/blog/zendesk-webhooks-guide/blog/receive-zendesk-webhooks-locally/) - [Zendesk webhook signature verifier](https://webhookrelay.com/blog/zendesk-webhooks-guide/verify-zendesk-webhook-signature/) — free, in-browser - [Verify a webhook signature](https://webhookrelay.com/blog/zendesk-webhooks-guide/blog/verify-webhook-signature/) - [What is a webhook](https://webhookrelay.com/blog/zendesk-webhooks-guide/blog/what-is-webhook/) --- --- title: Confluence Webhooks: Your Options on Cloud and Data Center meta: "og: title": "Confluence Webhooks: Your Options on Cloud and Data Center" description: How to get webhooks out of Confluence — Data Center's native webhooks, Cloud's Automation web requests and Forge apps — plus payloads and testing. url: https://webhookrelay.com/blog/confluence-webhooks.md file: /blog/confluence-webhooks.md --- ![Stripes](https://webhookrelay.com/blog/confluence-webhooks/images/stripes.svg) # **Confluence Webhooks: Your Options on Cloud and Data Center** How to get webhooks out of Confluence — Data Center's native webhooks, Cloud's Automation web requests and Forge apps — plus payloads and testing. ![Confluence webhooks guide](https://webhookrelay.com/blog/confluence-webhooks/images/blog/heroes/route.jpg) Jira makes webhooks easy; **Confluence** makes you choose your own adventure. There is no "Webhooks" page in Confluence Cloud's admin UI, which surprises everyone who arrives from Jira — yet you can absolutely get page and comment events pushed to your code. This guide maps the three real options and their trade-offs, then covers securing and testing them. (Both products share Atlassian's delivery infrastructure — the same `Atlassian Webhook HTTP Client` that sends [Jira webhooks](https://webhookrelay.com/blog/confluence-webhooks/blog/jira-webhooks-guide/).) ## [Option 1 (Cloud): Automation "Send web request"](#option-1-cloud-automation-send-web-request) The pragmatic choice for most teams. In **space settings → Automation** (or site-level automation): 1. Create a rule with a trigger — _Page published_, _Page updated_, _Comment added_… 2. Add the **Send web request** action. 3. Set your URL, method `POST`, content type JSON, and a body built from smart values: ``` { "event": "page_published", "pageId": "{{page.id}}", "title": "{{page.title}}", "space": "{{space.key}}", "author": "{{page.lastModifiedBy.displayName}}", "url": "{{page.url}}" } ``` You define the payload, so your handler parses exactly what you designed — and you can set an `Authorization` header, which doubles as your authentication (Automation requests carry no signature). Limits to know: automation rules have monthly execution caps by plan, and complex conditions count against them. ## [Option 2 (Cloud): a Forge or Connect app](#option-2-cloud-a-forge-or-connect-app) If you are building a product integration rather than internal glue, declare event subscriptions in an app. Forge apps subscribe to events like `avi:confluence:published:page` and get platform-managed delivery and auth; Connect apps register webhooks in their descriptor and receive JWT-authenticated calls. This is the marketplace-grade path — more setup, but no automation caps and proper identity. ## [Option 3 (Data Center/Server): native webhooks](#option-3-data-centerserver-native-webhooks) Self-hosted Confluence has what Cloud lacks: **Administration → Webhooks**, where you register a URL against events — page created/updated/removed, blog posts, comments, attachments, space and user events. Payloads are JSON with the entity and event metadata, and configuring a **secret** adds an HMAC signature header (the same `X-Hub-Signature` pattern Jira uses — verify HMAC-SHA256 over the raw body; test values in the free [HMAC verifier](https://webhookrelay.com/blog/confluence-webhooks/hmac-verification/)). ## [Which one?](#which-one) | Situation | Use | | --- | --- | | Internal notification/sync on Cloud, quickly | Automation web request | | Marketplace app or multi-tenant integration | Forge/Connect subscriptions | | Data Center/Server instance | Native webhooks | ## [Testing the flow](#testing-the-flow) Whatever the source, the receiving loop is the same as any webhook: 1. **Capture one first.** Point the web request (or webhook) at a free [Webhook Bin](https://webhookrelay.com/blog/confluence-webhooks/webhook-bin/) and trigger the event — you will see the exact headers and body before writing a line of handler code. 2. **Develop locally.** Confluence Cloud can only reach public URLs; forward deliveries to your machine with the relay agent (`relay forward --bucket confluence http://localhost:8080/webhook`) exactly as in the [Jira local guide](https://webhookrelay.com/blog/confluence-webhooks/blog/receive-jira-webhooks-locally/) — same Atlassian plumbing, same pattern. 3. **Replay** captured events while you iterate instead of re-publishing pages. ## [Reliability notes](#reliability-notes) Automation web requests are fire-and-forget — a failed request is logged in the rule's audit log but not aggressively retried, so if the destination matters, front it with a stable endpoint that stores deliveries and [retries towards your service](https://webhookrelay.com/blog/confluence-webhooks/features/durable-retries/). Data Center webhooks likewise treat delivery as best-effort; design your consumer to reconcile against the REST API when in doubt. ## [Related reading](#related-reading) - [Jira webhooks: setup, payload and security](https://webhookrelay.com/blog/confluence-webhooks/blog/jira-webhooks-guide/) — the sibling guide - [Test Jira webhooks locally](https://webhookrelay.com/blog/confluence-webhooks/blog/receive-jira-webhooks-locally/) - [How to test webhooks](https://webhookrelay.com/blog/confluence-webhooks/blog/how-to-test-webhooks/) - [What is a webhook](https://webhookrelay.com/blog/confluence-webhooks/blog/what-is-webhook/) --- --- title: Splunk Webhook Alerts: Payload, Limits and Fan-Out meta: "og: title": "Splunk Webhook Alerts: Payload, Limits and Fan-Out" description: Wire Splunk alert actions to webhooks: the exact JSON payload, the action's limits (no custom headers), URL allowlisting, and fan-out to Slack. url: https://webhookrelay.com/blog/splunk-webhook-alerts.md file: /blog/splunk-webhook-alerts.md --- ![Stripes](https://webhookrelay.com/blog/splunk-webhook-alerts/images/stripes.svg) # **Splunk Webhook Alerts: Payload, Limits and Fan-Out** Wire Splunk alert actions to webhooks: the exact JSON payload, the action's limits (no custom headers), URL allowlisting, and fan-out to Slack. ![Splunk webhook alerts](https://webhookrelay.com/blog/splunk-webhook-alerts/images/blog/heroes/route.jpg) Splunk's **webhook alert action** is the quickest way to get a saved-search alert out of Splunk and into your own systems — and also one of the most restrictive webhook senders you will meet: fixed payload, no custom headers, no retries to speak of. This guide shows exactly what it sends (from real captured traffic), the constraints, and the patterns that turn one Splunk alert into properly formatted notifications anywhere. ## [Setting it up](#setting-it-up) On a saved search: **Save As → Alert → Trigger Actions → Add Actions → Webhook**, then paste the target URL. That is the whole configuration surface — one URL field. Every time the alert triggers, Splunk POSTs JSON to it. **Splunk Cloud gotcha:** deliveries only go to allowlisted destinations. An admin adds your endpoint under **Server settings → Webhook allow list** first; otherwise the action silently fails (check `python.log`/`webhook` logs). ## [The payload](#the-payload) This is what actually arrives (shape captured on our public [webhook bin](https://webhookrelay.com/blog/splunk-webhook-alerts/webhook-bin/); values fictionalized): ``` { "sid": "scheduler__search__RMD5example_at_1768472000_100", "search_name": "High error rate on checkout service", "app": "search", "owner": "alerts", "results_link": "https://splunk.example.com:8000/app/search/search?q=%7Cloadjob%20…", "result": { "host": "web-03.example.internal", "source": "/var/log/app/error.log", "count": "147", "status_text": "error rate above threshold" } } ``` Key facts: - **`result` holds only the first row** of the alert's search results, fields as strings. If you need all rows, have the receiver follow `results_link` back into Splunk's API, or restructure the search so row one carries what you need. - The request comes from user agent `Splunk/` with `Content-Type: application/json`. - **There is no signature and no auth header.** Anyone who learns the URL can POST fake alerts, so treat the URL as a secret (path token) and/or validate the source IP. - The bin's sample catalog includes this exact event shape — one click sends a realistic Splunk alert at your handler while you develop. ## [The limitations — and the pattern around them](#the-limitations-and-the-pattern-around-them) The webhook action cannot: set headers, template the body, sign requests, or reshape JSON. Meanwhile real destinations want exactly those things — Slack wants `{"text": …}`, PagerDuty wants its Events API format, your internal gateway wants a bearer token. The clean solution is a **relay in the middle**. Point Splunk's one URL field at a stable Webhook Relay endpoint, then: - **Transform in flight** — a small JavaScript [transformation function](https://webhookrelay.com/blog/splunk-webhook-alerts/features/transform-webhooks/) reshapes Splunk's payload into the Slack/Teams/PagerDuty format and sets any headers your destination needs. - **Fan out** — deliver one alert to [multiple destinations](https://webhookrelay.com/blog/splunk-webhook-alerts/features/webhook-multiple-destinations/) at once: the on-call channel, the incident tool, and a data sink. - **Reach internal systems** — if the real consumer sits in a private network, the relay agent forwards alerts inside without opening firewall ports, the same pattern as [receiving webhooks on localhost](https://webhookrelay.com/blog/splunk-webhook-alerts/blog/how-to-test-webhooks/). - **Add reliability Splunk lacks** — deliveries are stored and [retried towards your destination for up to 30 days](https://webhookrelay.com/blog/splunk-webhook-alerts/features/durable-retries/), so a receiver restart doesn't drop the alert. One allowlist entry in Splunk Cloud covers every downstream destination, present and future. ## [Testing without triggering real alerts](#testing-without-triggering-real-alerts) 1. Capture one real firing into a [Webhook Bin](https://webhookrelay.com/blog/splunk-webhook-alerts/webhook-bin/) to see your instance's exact fields (custom search fields land in `result`). 2. Replay it against your handler while you iterate — or send the catalog's Splunk sample. 3. Wire the real alert to the relay endpoint once your handler behaves. ## [Related reading](#related-reading) - [Transform webhooks in flight](https://webhookrelay.com/blog/splunk-webhook-alerts/features/transform-webhooks/) - [Deliver one webhook to multiple destinations](https://webhookrelay.com/blog/splunk-webhook-alerts/features/webhook-multiple-destinations/) - [How to test webhooks](https://webhookrelay.com/blog/splunk-webhook-alerts/blog/how-to-test-webhooks/) - [What is a webhook](https://webhookrelay.com/blog/splunk-webhook-alerts/blog/what-is-webhook/) --- --- title: Receive GitLab Webhooks Locally | WebhookRelay meta: "og: title": "Receive GitLab Webhooks Locally" description: Receive GitLab webhooks locally with no public IP. Inspect the real payload, forward push and merge request events to localhost, and verify the token. url: https://webhookrelay.com/blog/receive-gitlab-webhooks-locally.md file: /blog/receive-gitlab-webhooks-locally.md --- ![Stripes](https://webhookrelay.com/blog/receive-gitlab-webhooks-locally/images/stripes.svg) # **Receive GitLab Webhooks Locally** Receive GitLab webhooks locally with no public IP. Inspect the real payload, forward push and merge request events to localhost, and verify the token. ![Receive GitLab Webhooks Locally (No Public IP Required)](https://webhookrelay.com/blog/receive-gitlab-webhooks-locally/images/blog/heroes/localhost.jpg) You are wiring up a GitLab integration — a chatops bot, a custom deploy trigger, a pipeline notifier — and you need to watch your handler react to a real `push` or `merge_request` event. But there is an immediate wall: GitLab will only POST to a **public URL**, and your handler is running on `localhost:8080` behind a NAT or a corporate firewall with **no public IP**. Deploying to a staging server for every change is slow. Copying a sample payload from the docs gives you a guess, not the real headers and body GitLab sends. What you want is to **receive GitLab webhooks locally** — real events hitting your local handler, over a URL that does not change every time you restart. Here is how to set that up. ## [Why receiving GitLab webhooks locally is tricky](#why-receiving-gitlab-webhooks-locally-is-tricky) A webhook is just an HTTP request GitLab sends to a URL when something happens in your project. GitLab is on the public internet; your dev machine almost never is. It sits behind a router or firewall with no inbound ports open and no routable address. So you need a public endpoint GitLab can hit that relays each request down to your laptop — without opening any firewall ports. That is what [Webhook Relay](https://my.webhookrelay.com/register) does, and the endpoint is **stable**: configure GitLab once and forget about it. ## [Step 1: Inspect the real payload with Webhook Bin](#step-1-inspect-the-real-payload-with-webhook-bin) Before writing handler code, see what GitLab actually sends. Open the free [Webhook Bin](https://webhookrelay.com/blog/receive-gitlab-webhooks-locally/webhook-bin/) — no signup — for an instant public URL. 1. Copy the Webhook Bin URL. 2. In your project, go to **Settings → Webhooks → Add new webhook**. 3. Paste the URL into the **URL** field and select the trigger events you want — **Push events**, **Merge request events**, **Pipeline events**, **Tag push**, and so on. 4. Save the webhook, then click **Test** and pick an event (for example, **Push events**). GitLab immediately sends a sample payload. The sample lands in Webhook Bin instantly. Inspect the full JSON body and every header, including `X-Gitlab-Event` (which event fired) and `X-Gitlab-Token` (your secret). Now you know the exact data shape before writing a line of code. See [How to test webhooks](https://webhookrelay.com/blog/receive-gitlab-webhooks-locally/blog/how-to-test-webhooks/) and [What is a webhook](https://webhookrelay.com/blog/receive-gitlab-webhooks-locally/blog/what-is-webhook/) for background. ## [Step 2: Forward the events to localhost with the relay agent](#step-2-forward-the-events-to-localhost-with-the-relay-agent) Now route those events into your local handler. [Sign up for Webhook Relay](https://my.webhookrelay.com/register), install the `relay` agent (CLI or Docker), and create a bucket — say `gitlab`. The bucket gives you a stable public input endpoint. Start forwarding to your local server: ``` relay forward --bucket gitlab http://localhost:8080/webhook ``` The agent opens an **outbound** connection to Webhook Relay and streams every incoming request down to `http://localhost:8080/webhook`. Because the connection is outbound, there are **no firewall ports to open and no public IP needed** — this works from your laptop, behind a corporate proxy, or inside a Kubernetes cluster. The official `webhookrelay/webhookrelayd` Docker image runs the same command. Full details are in the [localhost forwarding docs](https://webhookrelay.com/blog/receive-gitlab-webhooks-locally/docs/webhooks/internal/localhost/). Now update the GitLab webhook's **URL** to your Webhook Relay endpoint (or create it there from the start), hit **Test** again, and watch the event arrive on `localhost`. ## [GitLab-specific configuration and quirks](#gitlab-specific-configuration-and-quirks) A few GitLab details to get right: - **Where to add it:** project **Settings → Webhooks** for a single project, or **group Settings → Webhooks** to cover every project in the group. - **Trigger events:** tick the events you need — push, tag push, merge request, pipeline, issue, comment, deployment, and more. Use the `X-Gitlab-Event` header to branch in your handler. - **The Test button:** GitLab can send a representative sample for any trigger type on demand. This is the fastest way to confirm your endpoint is reachable and your handler parses the body — no need to manufacture a real push. - **Secret token, not a signature:** this is the key difference from GitHub. GitLab does **not** compute an HMAC over the body. Instead, it sends the **secret token you configured** verbatim in the **`X-Gitlab-Token`** header. That makes verification a direct comparison rather than a recomputed digest. ## [Step 3: Verify the GitLab token](#step-3-verify-the-gitlab-token) Because GitLab sends the raw token, verification is straightforward: read the **`X-Gitlab-Token`** header and compare it against the secret you stored, using a constant-time comparison to avoid timing attacks. Reject the request if it does not match. Two notes: - Unlike GitHub's `X-Hub-Signature-256`, there is **no HMAC to recompute** — GitLab's value is the secret itself, so do not run it through a hashing step. - Always serve your endpoint over HTTPS so the token is not exposed in transit. Webhook Relay endpoints are HTTPS by default. For the broader pattern — and the GitHub-style HMAC approach if you also handle signed providers — see [Verify a webhook signature](https://webhookrelay.com/blog/receive-gitlab-webhooks-locally/blog/verify-webhook-signature/) and the free [HMAC signature verifier](https://webhookrelay.com/blog/receive-gitlab-webhooks-locally/hmac-verification/). ## [Replay and iterate](#replay-and-iterate) This is where local development gets fast: - **Re-fire from GitLab** with the **Test** button to send a fresh sample event whenever you want. - **Replay from Webhook Relay** — past requests are stored on your bucket, so you can resend a captured event (including its `X-Gitlab-Token` header) without touching GitLab at all. - **Iterate on your handler** by editing code and replaying the same event until it behaves correctly. No staging deploys, no merge requests just to test a code path. Because the Webhook Relay endpoint is stable, you can stop and restart the agent, reboot your machine, or pick this back up next week — the GitLab configuration never needs to change. ## [Get started](#get-started) 1. Inspect the real payload in the free [Webhook Bin](https://webhookrelay.com/blog/receive-gitlab-webhooks-locally/webhook-bin/) — no signup needed. 2. [Create a Webhook Relay account](https://my.webhookrelay.com/register), install the agent, and run `relay forward --bucket gitlab http://localhost:8080/webhook`. 3. Point your GitLab webhook at the stable endpoint, hit **Test**, and watch the event hit `localhost`. You will be testing real GitLab events against your local handler in minutes — no deploys, no open firewall ports, and no public IP required. --- --- title: Receive Twilio Webhooks Locally | WebhookRelay meta: "og: title": "Receive Twilio Webhooks Locally" description: Test Twilio webhooks without deploying. Forward SMS and voice callbacks to localhost, handle the form-encoded body, and verify X-Twilio-Signature. url: https://webhookrelay.com/blog/receive-twilio-webhooks-locally.md file: /blog/receive-twilio-webhooks-locally.md --- ![Stripes](https://webhookrelay.com/blog/receive-twilio-webhooks-locally/images/stripes.svg) # **Receive Twilio Webhooks Locally** Test Twilio webhooks without deploying. Forward SMS and voice callbacks to localhost, handle the form-encoded body, and verify X-Twilio-Signature. ![Receive Twilio Webhooks Locally: Test Twilio Webhooks on localhost](https://webhookrelay.com/blog/receive-twilio-webhooks-locally/images/blog/heroes/localhost.jpg) Building a Twilio integration means your code has to answer an HTTP request the moment someone texts or calls your number. The problem is obvious the first time you try it: Twilio needs a **public URL**, and your handler is running on `localhost`. You don't want to deploy to a staging server, redeploy on every change, and dig through remote logs just to see one inbound SMS. This guide shows how to **receive Twilio webhooks locally** — forwarding live incoming SMS and voice callbacks straight to your machine so you can build and debug your handler entirely on localhost. ## [Why testing Twilio webhooks is tricky](#why-testing-twilio-webhooks-is-tricky) Twilio webhooks are _inbound_ HTTP requests. When a message hits your number, Twilio POSTs to whatever URL you configured and expects a response (usually [TwiML](https://www.twilio.com/docs/messaging/twiml)) back within seconds. Two things make this awkward in development: 1. **Twilio needs a stable, public, HTTPS endpoint** — it can't reach `localhost:3000` on your laptop. 2. **The payload isn't JSON.** Incoming SMS and voice webhooks are sent as `application/x-www-form-urlencoded`, so if you assume JSON you'll get an empty body and a confusing bug. Webhook Relay solves the connectivity half: it gives you a stable public URL and forwards every request to your local server over an outbound connection — no firewall ports, no public IP. ## [Step 1: Inspect the payload with a Webhook Bin](#step-1-inspect-the-payload-with-a-webhook-bin) Before you write a single line of handler code, look at what Twilio actually sends. Open the free [Webhook Bin](https://webhookrelay.com/blog/receive-twilio-webhooks-locally/webhook-bin/) — you get an instant URL with no signup. Paste that URL into a Twilio number's Messaging webhook, text the number, and watch the request appear in your browser. You'll see the request is a `POST` with `Content-Type: application/x-www-form-urlencoded` and form fields like: ``` MessageSid=SMxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx From=%2B15551234567 To=%2B15557654321 Body=Hello+from+Twilio NumMedia=0 ``` You'll also see the `X-Twilio-Signature` header. Now you know exactly what to parse — no guessing. ## [Step 2: Forward webhooks to localhost with the relay agent](#step-2-forward-webhooks-to-localhost-with-the-relay-agent) Once you're ready to hit real code, install the [relay agent](https://webhookrelay.com/blog/receive-twilio-webhooks-locally/docs/webhooks/internal/localhost/) (CLI or Docker), sign in, and forward to your local port. Say your handler listens on `http://localhost:3000/sms`: ``` relay forward --bucket twilio http://localhost:3000/sms ``` The agent prints a **stable public URL** like `https://hook.relay.sh/v1/webhooks/...`. Because the connection is outbound from your machine, you don't open any ports, and the URL stays the same across restarts — so you only configure Twilio once. ## [Step 3: Configure the webhook in the Twilio Console](#step-3-configure-the-webhook-in-the-twilio-console) Twilio webhook URLs are configured **per resource**, not globally: - **A phone number:** Console → **Phone Numbers** → **Manage** → **Active Numbers** → select the number → set the **A message comes in** (Messaging) or **A call comes in** (Voice) webhook to your Webhook Relay URL, method `HTTP POST`. - **A Messaging Service:** Console → **Messaging** → **Services** → your service → **Integration** → point it at the same URL. - **A TwiML App:** Console → **Voice** → **TwiML Apps**, used for voice apps and clients. Save, then text or call your number. The request travels Twilio → Webhook Relay → your `localhost:3000/sms` handler. ## [Step 4: Handle the form body and reply with TwiML](#step-4-handle-the-form-body-and-reply-with-twiml) This is where most first attempts break. **Read the body as form data, not JSON.** A minimal Express handler: ``` const express = require("express"); const app = express(); // Twilio posts application/x-www-form-urlencoded app.use(express.urlencoded({ extended: false })); app.post("/sms", (req, res) => { const from = req.body.From; const text = req.body.Body; console.log(\`SMS from ${from}: ${text}\`); // Respond with TwiML (XML), not JSON res.type("text/xml"); res.send(\` Got your message: ${text} \`); }); app.listen(3000, () => console.log("listening on :3000")); ``` The response is **TwiML** — XML with a `Content-Type` of `text/xml` — telling Twilio what to do next (reply, forward, hang up). Returning JSON here is a common mistake. ## [Step 5: Verify the X-Twilio-Signature](#step-5-verify-the-x-twilio-signature) In production you should confirm each request really came from Twilio. Twilio signs every webhook with **HMAC-SHA1** using your account **Auth Token** as the key. The signed string is: > the full request **URL** + every POST parameter, **sorted alphabetically by name** and concatenated as `name\`\`value` with no delimiters …then HMAC-SHA1'd and **base64-encoded** into the `X-Twilio-Signature` header. The critical gotcha when developing locally: **sign against the public URL Twilio actually called** — your Webhook Relay URL — not the internal `http://localhost:3000/sms` URL. If you compute the signature over the localhost URL, it will never match. Either configure your validation with the public URL or set your framework to trust the forwarded host. Twilio's official helper libraries (`twilio` for Node, Python, etc.) provide a `RequestValidator` that does the HMAC-SHA1 + base64 work for you — just pass the public URL, the parsed POST params, and the header. To understand what's happening under the hood, see our [webhook signature verification guide](https://webhookrelay.com/blog/receive-twilio-webhooks-locally/blog/verify-webhook-signature/), and use the free [Twilio signature verifier](https://webhookrelay.com/blog/receive-twilio-webhooks-locally/verify-twilio-webhook-signature/) to check a signature by hand while debugging. ## [Step 6: Replay and iterate](#step-6-replay-and-iterate) You don't want to text your number a hundred times while fixing a parser. Every request that flows through Webhook Relay is captured, so you can **resend** the exact same form-encoded payload to your handler as many times as you like. Tweak your code, replay, repeat — all on localhost, with no source-side event needed. For the broader workflow, see [how to test webhooks](https://webhookrelay.com/blog/receive-twilio-webhooks-locally/blog/how-to-test-webhooks/). ## [Wrapping up](#wrapping-up) Receiving Twilio webhooks on localhost comes down to three things: a stable public URL that forwards to your machine, parsing the **form-encoded** body (not JSON) and replying with TwiML, and validating `X-Twilio-Signature` against the **public** URL. With Webhook Relay you get all of that without deploying or opening a single firewall port. For the complete reference — configuration, payload fields, signature validation and retries — see the [Twilio webhooks guide](https://webhookrelay.com/blog/receive-twilio-webhooks-locally/blog/twilio-webhooks-guide/). [Sign up for free](https://my.webhookrelay.com/register) to start forwarding to localhost, or grab an instant [Webhook Bin](https://webhookrelay.com/blog/receive-twilio-webhooks-locally/webhook-bin/) URL to inspect your first Twilio request right now. --- --- title: Receive Slack Events on localhost | WebhookRelay meta: "og: title": "Receive Slack Events on localhost" description: Test Slack webhooks without deploying. Forward Events API requests to localhost, pass the url_verification challenge, and verify X-Slack-Signature. url: https://webhookrelay.com/blog/receive-slack-events-locally.md file: /blog/receive-slack-events-locally.md --- ![Stripes](https://webhookrelay.com/blog/receive-slack-events-locally/images/stripes.svg) # **Receive Slack Events Locally: Test Slack Webhooks on localhost** Test Slack webhooks without deploying. Forward Events API requests to localhost, pass the url_verification challenge, and verify X-Slack-Signature. ![Receive Slack Events Locally: Test Slack Webhooks on localhost](https://webhookrelay.com/blog/receive-slack-events-locally/images/blog/heroes/localhost.jpg) Slack's Events API, slash commands, and interactivity all work the same way: Slack POSTs to a **Request URL** you configure, and your app responds. Which means the moment you start building a Slack app, you hit the classic wall — Slack needs a **public URL**, but your handler is on `localhost`, and you'd rather not deploy to staging on every change just to see one event. This guide shows how to **receive Slack events locally** — forwarding live Events API requests, slash commands, and interactions straight to your machine so you can develop and debug entirely on localhost. ## [Why testing Slack webhooks is tricky](#why-testing-slack-webhooks-is-tricky) There are two specific hurdles beyond plain connectivity: 1. **Slack needs a public, reachable Request URL** — it can't POST to `localhost:3000`. 2. **Slack verifies the URL with a `url_verification` challenge** before it will deliver any events. If your handler doesn't echo the challenge back correctly and quickly, Slack marks the URL as failed and you're stuck on the config screen. Webhook Relay handles the connectivity: a stable public URL forwarding to your local server over an outbound connection — no firewall ports, no public IP. Your handler still needs to answer the challenge, which we'll cover below. ## [Step 1: Inspect the request with a Webhook Bin](#step-1-inspect-the-request-with-a-webhook-bin) Start by seeing exactly what Slack sends. Open the free [Webhook Bin](https://webhookrelay.com/blog/receive-slack-events-locally/webhook-bin/) for an instant URL (no signup), paste it as your Request URL under **Event Subscriptions**, and watch Slack's first request land in your browser. The very first request you'll see is the verification handshake — a JSON `POST`: ``` { "token": "Jhj5dZrVaK7ZwHHjRyZWjbDl", "challenge": "3eZbrw1aBm2rZgRNFdxV2595E9CY3gmdALWMmHkvFXO7tYXAYM8P", "type": "url_verification" } ``` You'll also see the `X-Slack-Signature` and `X-Slack-Request-Timestamp` headers on every request. Now you know exactly what to build against. ## [Step 2: Forward webhooks to localhost with the relay agent](#step-2-forward-webhooks-to-localhost-with-the-relay-agent) When you're ready to run real code, install the [relay agent](https://webhookrelay.com/blog/receive-slack-events-locally/docs/webhooks/internal/localhost/) (CLI or Docker), sign in, and forward to your local port. If your app listens on `http://localhost:3000/slack/events`: ``` relay forward --bucket slack http://localhost:3000/slack/events ``` The agent prints a **stable public URL** like `https://hook.relay.sh/v1/webhooks/...`. The connection is outbound, so no ports are opened, and the URL persists across restarts — handy because Slack re-runs the challenge every time you change the Request URL. ## [Step 3: Configure the Request URL in your Slack app](#step-3-configure-the-request-url-in-your-slack-app) Go to [api.slack.com/apps](https://api.slack.com/apps) → your app, then: - **Events API:** **Event Subscriptions** → toggle on → set **Request URL** to your Webhook Relay URL. Slack immediately fires the `url_verification` challenge. Below it, subscribe to the bot/workspace events you care about (e.g. `message.channels`, `app_mention`). - **Slash commands:** **Slash Commands** → create/edit a command → set its **Request URL** to the same Webhook Relay URL (commands POST `application/x-www-form-urlencoded`). - **Interactivity:** **Interactivity & Shortcuts** → toggle on → set the **Request URL** for button clicks, modals, and shortcuts. ## [Step 4: Answer the url_verification challenge](#step-4-answer-the-url_verification-challenge) Slack won't deliver events until your endpoint passes the handshake. When you receive a body with `"type": "url_verification"`, **respond within a few seconds, echoing the exact `challenge` value** back. Plain text or JSON both work: ``` const express = require("express"); const app = express(); app.use(express.json()); app.post("/slack/events", (req, res) => { // 1. Handle the verification handshake if (req.body.type === "url_verification") { return res.type("text/plain").send(req.body.challenge); } // 2. Normal events const event = req.body.event; if (event && event.type === "app_mention") { console.log(\`Mentioned in ${event.channel}: ${event.text}\`); } // Slack expects a fast 200 — ack now, do work async res.sendStatus(200); }); app.listen(3000, () => console.log("listening on :3000")); ``` Two practical notes: Slack expects a `200` **quickly** (do heavy work asynchronously, after acking), and for the challenge you can also return `{"challenge": "..."}` as JSON if you prefer. ## [Step 5: Verify the X-Slack-Signature](#step-5-verify-the-x-slack-signature) Once events flow, confirm they actually came from Slack. Slack signs every request with **HMAC-SHA256** using your app's **Signing Secret**. The base string is built by joining three parts with colons: ``` v0:{X-Slack-Request-Timestamp}:{raw_request_body} ``` HMAC-SHA256 that string with your signing secret, hex-encode it, prefix `v0=`, and compare to the `X-Slack-Signature` header. Two things matter a lot: - **Compute over the raw, unparsed body bytes.** If middleware reparses or reserializes the body first, the signature won't match. Capture the raw body before JSON parsing. - **Check the timestamp.** Reject requests where `X-Slack-Request-Timestamp` is more than ~5 minutes from now — this is Slack's built-in replay protection. For a walkthrough of HMAC signature checking in general, see our [webhook signature verification guide](https://webhookrelay.com/blog/receive-slack-events-locally/blog/verify-webhook-signature/), and use the free [Slack signature verifier](https://webhookrelay.com/blog/receive-slack-events-locally/verify-slack-webhook-signature/) to validate a signature by hand while you debug. ## [Step 6: Replay and iterate](#step-6-replay-and-iterate) You don't want to spam a channel to trigger events while fixing your handler. Every request through Webhook Relay is captured, so you can **resend** the exact same payload — challenge or real event — to your local server as many times as you need. Change code, replay, repeat, all on localhost. See [how to test webhooks](https://webhookrelay.com/blog/receive-slack-events-locally/blog/how-to-test-webhooks/) for the full workflow. ## [Wrapping up](#wrapping-up) Receiving Slack events on localhost comes down to: a stable public Request URL that forwards to your machine, **passing the `url_verification` challenge** by echoing it back fast, and validating `X-Slack-Signature` (HMAC-SHA256 over `v0:timestamp:body`) against the raw body. Webhook Relay gives you the public URL and live forwarding without deploying or opening a port. [Sign up for free](https://my.webhookrelay.com/register) to forward Slack events to localhost, or grab an instant [Webhook Bin](https://webhookrelay.com/blog/receive-slack-events-locally/webhook-bin/) URL to inspect Slack's challenge request right now. --- --- title: Test SendGrid Webhooks Locally | WebhookRelay meta: "og: title": "Test SendGrid Webhooks Locally" description: Test the SendGrid Event Webhook on localhost without deploying. Inspect the real JSON event array and verify the signed webhook signature. url: https://webhookrelay.com/blog/receive-sendgrid-webhooks-locally.md file: /blog/receive-sendgrid-webhooks-locally.md --- ![Stripes](https://webhookrelay.com/blog/receive-sendgrid-webhooks-locally/images/stripes.svg) # **Test SendGrid Webhooks Locally** Test the SendGrid Event Webhook on localhost without deploying. Inspect the real JSON event array and verify the signed webhook signature. ![Test SendGrid Webhooks Locally (SendGrid Event Webhook on localhost)](https://webhookrelay.com/blog/receive-sendgrid-webhooks-locally/images/blog/heroes/localhost.jpg) You are building a SendGrid integration — updating delivery status, tracking opens and clicks, suppressing addresses that bounce — and you need to see your handler react to a real batch of email events. The problem is immediate: SendGrid will only POST to a **public URL**, and your handler is running on `localhost:8080`. SendGrid has no way to reach it. The usual workarounds are painful. Deploying to a staging server for every code change is slow. Pasting payloads from the docs into curl gives you a _guess_ at the real request, not the real headers and body SendGrid actually sends. What you want is to **test SendGrid webhooks locally** — real events, hitting your local handler, with a URL that does not change every time you restart. This guide shows how to do exactly that. ## [Why testing SendGrid webhooks locally is tricky](#why-testing-sendgrid-webhooks-locally-is-tricky) The SendGrid Event Webhook is just an HTTP request that SendGrid sends to a URL whenever your email moves through its pipeline — processed, delivered, opened, clicked, bounced. SendGrid sits on the public internet; your dev machine usually does not. It is behind a router, a corporate firewall, or both, with no public IP and no inbound ports open. So you need something in the middle: a public endpoint SendGrid can hit, that relays each request down to your laptop without you opening a single firewall port. That is what [Webhook Relay](https://my.webhookrelay.com/register) does — and unlike a random tunnel URL, the endpoint is **stable**, so you configure SendGrid once and never touch it again. ## [Step 1: Inspect the real payload with Webhook Bin](#step-1-inspect-the-real-payload-with-webhook-bin) Before you write any handler code, find out what SendGrid actually sends. Open the free [Webhook Bin](https://webhookrelay.com/blog/receive-sendgrid-webhooks-locally/webhook-bin/) — no signup — and you get an instant public URL. 1. Copy the Webhook Bin URL. 2. In the SendGrid dashboard, go to **Settings → Mail Settings → Event Webhook** (in some accounts this lives under **Settings → Tracking**). 3. Paste the URL into the **HTTP POST URL** field and select the events you care about — `processed`, `delivered`, `open`, `click`, `bounce`, `dropped`, `deferred`, `spamreport`, `unsubscribe`. 4. Toggle the Event Webhook **on** and save. SendGrid provides a **Test Your Integration** button that POSTs a sample batch immediately — you will see it land in Webhook Bin right away. Inspect the captured request and note the most important detail: the body is a **JSON array** of event objects, not a single object. Each object has fields like `email`, `event`, `timestamp`, `sg_event_id`, `sg_message_id`, and (for engagement events) a `category`. The headers include `X-Twilio-Email-Event-Webhook-Signature` and `X-Twilio-Email-Event-Webhook-Timestamp` when signing is enabled. Now you know the exact shape of the data before writing a line of code. For more on this approach, see [How to test webhooks](https://webhookrelay.com/blog/receive-sendgrid-webhooks-locally/blog/how-to-test-webhooks/) and [What is a webhook](https://webhookrelay.com/blog/receive-sendgrid-webhooks-locally/blog/what-is-webhook/). ## [Step 2: Forward the events to localhost with the relay agent](#step-2-forward-the-events-to-localhost-with-the-relay-agent) Once you know the payload, route those same events into your local handler. [Sign up for Webhook Relay](https://my.webhookrelay.com/register), install the `relay` agent (CLI or Docker), and create a bucket — say `sendgrid`. The bucket gives you a stable public input endpoint. Start forwarding to your local server: ``` relay forward --bucket sendgrid http://localhost:8080/webhook ``` The agent opens an **outbound** connection to Webhook Relay and streams every incoming request down to `http://localhost:8080/webhook`. Because the connection is outbound, there are **no firewall ports to open and no public IP needed** — this works from your laptop, behind a corporate proxy, or inside a Kubernetes cluster. Running in Docker? The same command works in the official `webhookrelay/webhookrelayd` image. Full details are in the [localhost forwarding docs](https://webhookrelay.com/blog/receive-sendgrid-webhooks-locally/docs/webhooks/internal/localhost/). Now update the Event Webhook's **HTTP POST URL** to your Webhook Relay endpoint (or just create it there from the start). Click **Test Your Integration** again — or send yourself a real email — and watch the event batch arrive on `localhost`. ## [SendGrid-specific configuration and quirks](#sendgrid-specific-configuration-and-quirks) A few SendGrid details worth knowing: - **Where to add it:** the Event Webhook is configured under **Settings → Mail Settings → Event Webhook** (or **Settings → Tracking** in older accounts). Newer accounts can also create multiple Event Webhooks via the API. - **Batched JSON array:** SendGrid groups multiple events into a **single POST as a JSON array**. Your handler must iterate the array — a parser that expects one object per request will silently drop events. - **Event types:** subscribe only to what you handle. Delivery events (`processed`, `delivered`, `bounce`, `dropped`, `deferred`) and engagement events (`open`, `click`, `unsubscribe`, `spamreport`) are toggled separately. - **Deduplicate on `sg_event_id`:** SendGrid retries on non-`2xx` responses, so the same event can arrive more than once. Use `sg_event_id` for idempotency and return `2xx` quickly. - **Inbound Parse is different:** the [Inbound Parse webhook](https://www.twilio.com/docs/sendgrid/for-developers/parsing-email/setting-up-the-inbound-parse-webhook) is a separate feature for _receiving_ inbound email — it POSTs `multipart/form-data` to your endpoint, not the JSON event array. You can forward it through the same Webhook Relay bucket, but parse it differently. ## [Step 3: Verify the Signed Event Webhook signature](#step-3-verify-the-signed-event-webhook-signature) SendGrid's signature feature is **ECDSA, not HMAC** — an important distinction if you have only ever verified HMAC webhooks. When you enable the **Signed Event Webhook** in Mail Settings, SendGrid generates an elliptic-curve key pair, signs each request with the **private** key, and gives you a **public verification key** in the dashboard. Each request carries the signature in the **`X-Twilio-Email-Event-Webhook-Signature`** header and a timestamp in **`X-Twilio-Email-Event-Webhook-Timestamp`**. To verify, you reconstruct the signed payload as the **timestamp concatenated with the raw request body**, then check the ECDSA signature against your public key. As always, use the **raw** bytes of the body — re-serialized JSON will not match. SendGrid's official libraries ship an `eventwebhook` helper that wraps this for you. Because this is ECDSA rather than a shared-secret HMAC, the generic [HMAC signature verifier](https://webhookrelay.com/blog/receive-sendgrid-webhooks-locally/hmac-verification/) does not apply directly to SendGrid's signed webhook — but it remains a handy tool for the many HMAC-based providers. For the broader concepts, language-specific code, and the common pitfalls (reading the body after a JSON parser has consumed it, forgetting the timestamp, timing-safe comparison), read [Verify a webhook signature](https://webhookrelay.com/blog/receive-sendgrid-webhooks-locally/blog/verify-webhook-signature/). ## [Replay and iterate](#replay-and-iterate) This is where local development gets fast: - **Replay from SendGrid** by hitting **Test Your Integration** again, or by sending a fresh test email. - **Replay from Webhook Relay** — past requests are stored on your bucket, so you can resend a captured event batch without touching SendGrid at all. - **Iterate on your handler** by editing code and replaying the same delivery until it behaves correctly. No commits, no pushes, no deploys just to test a code path. Because the Webhook Relay endpoint is stable, you can stop and restart the agent, reboot your machine, or come back next week — the SendGrid configuration never needs to change. ## [Get started](#get-started) 1. Inspect the real payload in the free [Webhook Bin](https://webhookrelay.com/blog/receive-sendgrid-webhooks-locally/webhook-bin/) — no signup needed. 2. [Create a Webhook Relay account](https://my.webhookrelay.com/register), install the agent, and run `relay forward --bucket sendgrid http://localhost:8080/webhook`. 3. Point your SendGrid Event Webhook's HTTP POST URL at the stable endpoint, click Test Your Integration, and watch the event batch hit `localhost`. You will be testing real SendGrid events against your local handler in a few minutes — no deploys, no open firewall ports, and a URL you configure exactly once. --- --- title: Receive Square Webhooks Locally | WebhookRelay meta: "og: title": "Receive Square Webhooks Locally" description: Receive Square webhooks on localhost without deploying. Inspect the real payment.created payload and verify the HMAC signature on every request. url: https://webhookrelay.com/blog/receive-square-webhooks-locally.md file: /blog/receive-square-webhooks-locally.md --- ![Stripes](https://webhookrelay.com/blog/receive-square-webhooks-locally/images/stripes.svg) # **Receive Square Webhooks Locally** Receive Square webhooks on localhost without deploying. Inspect the real payment.created payload and verify the HMAC signature on every request. ![Receive Square Webhooks Locally (Test Square Webhooks on localhost)](https://webhookrelay.com/blog/receive-square-webhooks-locally/images/blog/heroes/localhost.jpg) You are building a Square integration — recording a sale, fulfilling an order, reconciling a refund — and you need to see your handler react to a real `payment.created` or `order.updated` event. The problem is immediate: Square will only POST to a **public URL**, and your handler is running on `localhost:8080`. Square has no way to reach it. The usual workarounds are painful. Deploying to a staging server for every code change is slow. Pasting payloads from the docs into curl gives you a _guess_ at the real request, not the real headers and body Square actually sends. What you want is to **receive Square webhooks locally** — real events, hitting your local handler, with a URL that does not change every time you restart. This guide shows how to do exactly that. ## [Why receiving Square webhooks locally is tricky](#why-receiving-square-webhooks-locally-is-tricky) A webhook is just an HTTP request that Square sends to a URL when something happens in a seller's account. Square sits on the public internet; your dev machine usually does not. It is behind a router, a corporate firewall, or both, with no public IP and no inbound ports open. So you need something in the middle: a public endpoint Square can hit, that relays each request down to your laptop without you opening a single firewall port. That is what [Webhook Relay](https://my.webhookrelay.com/register) does — and unlike a random tunnel URL, the endpoint is **stable**, so you configure Square once and never touch it again. ## [Step 1: Inspect the real payload with Webhook Bin](#step-1-inspect-the-real-payload-with-webhook-bin) Before you write any handler code, find out what Square actually sends. Open the free [Webhook Bin](https://webhookrelay.com/blog/receive-square-webhooks-locally/webhook-bin/) — no signup — and you get an instant public URL. 1. Copy the Webhook Bin URL. 2. In the [Square Developer Dashboard](https://developer.squareup.com/), open your application and go to **Webhooks → Subscriptions → Add Subscription** (or **Add Endpoint**). 3. Paste the URL into **Notification URL**, choose the **API version**, and select the event types you care about — for example `payment.created`, `payment.updated`, `order.updated`, `refund.created`, or `invoice.payment_made`. 4. Save the subscription. Now click **Send Test Event** and pick an event type. Square immediately fires a sample notification, and you will see it land in Webhook Bin right away. Inspect the captured request: the full JSON body (with its `merchant_id`, `type`, `event_id`, and nested `data.object`), and every header, including `x-square-hmacsha256-signature` and `square-environment`. Now you know the exact shape of the data before writing a line of code. For more on this approach, see [How to test webhooks](https://webhookrelay.com/blog/receive-square-webhooks-locally/blog/how-to-test-webhooks/) and [What is a webhook](https://webhookrelay.com/blog/receive-square-webhooks-locally/blog/what-is-webhook/). ## [Step 2: Forward the events to localhost with the relay agent](#step-2-forward-the-events-to-localhost-with-the-relay-agent) Once you know the payload, route those same events into your local handler. [Sign up for Webhook Relay](https://my.webhookrelay.com/register), install the `relay` agent (CLI or Docker), and create a bucket — say `square`. The bucket gives you a stable public input endpoint. Start forwarding to your local server: ``` relay forward --bucket square http://localhost:8080/webhook ``` The agent opens an **outbound** connection to Webhook Relay and streams every incoming request down to `http://localhost:8080/webhook`. Because the connection is outbound, there are **no firewall ports to open and no public IP needed** — this works from your laptop, behind a corporate proxy, or inside a Kubernetes cluster. Running in Docker? The same command works in the official `webhookrelay/webhookrelayd` image. Full details are in the [localhost forwarding docs](https://webhookrelay.com/blog/receive-square-webhooks-locally/docs/webhooks/internal/localhost/). Now update the subscription's **Notification URL** to your Webhook Relay endpoint (or just create it there from the start). Hit **Send Test Event** again — or trigger a real action in the sandbox — and watch it arrive on `localhost`. ## [Square-specific configuration and quirks](#square-specific-configuration-and-quirks) A few Square details worth knowing: - **Where to add it:** webhook subscriptions live in the **Square Developer Dashboard → your application → Webhooks → Subscriptions**. Each subscription has its own notification URL, event list, and signature key. - **Sandbox vs production:** Square keeps **separate** credentials and subscriptions for the **Sandbox** and **Production** environments. Test against the sandbox first, then move the subscription to production — the signature key is different per environment, so update your handler's config accordingly. The `square-environment` header tells you which one sent the request. - **Event types:** subscribe to exactly the events you handle (`payment.created`, `order.updated`, `refund.updated`, `customer.created`, and so on). Branch on the `type` field in the JSON body rather than parsing the path. - **Send Test Event:** the dashboard's **Send Test Event** button delivers a representative payload for any subscribed event type without needing to actually take a payment — perfect for wiring up and debugging your handler. - **Retries:** if your endpoint does not return a `2xx` quickly, Square retries with backoff. Returning `200` fast and processing asynchronously keeps deliveries clean. ## [Step 3: Verify the Square webhook signature](#step-3-verify-the-square-webhook-signature) Square signs every notification. It computes an **HMAC-SHA256** over the **notification URL concatenated with the raw request body**, using your subscription's **signature key**, then base64-encodes the result and sends it in the **`x-square-hmacsha256-signature`** header. That URL-plus-body detail trips people up: you must use the _exact_ notification URL configured on the subscription and the **raw**, unparsed request body (no re-serialized JSON, no added whitespace). Find the signature key under **Webhooks → Subscriptions → your endpoint → Show** (in the Signature Key box). Recompute the digest and compare in constant time before trusting the payload. To sanity-check your implementation, paste a captured body, your signature key, and the received signature into the free [Square signature verifier](https://webhookrelay.com/blog/receive-square-webhooks-locally/verify-square-webhook-signature/). For language-specific code and the common pitfalls (reading the body after a JSON parser has consumed it, forgetting to prepend the URL, timing-safe comparison), read [Verify a webhook signature](https://webhookrelay.com/blog/receive-square-webhooks-locally/blog/verify-webhook-signature/). ## [Replay and iterate](#replay-and-iterate) This is where local development gets fast: - **Replay from Square** by hitting **Send Test Event** again, or by re-triggering an action in the sandbox. - **Replay from Webhook Relay** — past requests are stored on your bucket, so you can resend a captured event without touching Square at all. - **Iterate on your handler** by editing code and replaying the same delivery until it behaves correctly. No commits, no pushes, no deploys just to test a code path. Because the Webhook Relay endpoint is stable, you can stop and restart the agent, reboot your machine, or come back next week — the Square configuration never needs to change. ## [Get started](#get-started) 1. Inspect the real payload in the free [Webhook Bin](https://webhookrelay.com/blog/receive-square-webhooks-locally/webhook-bin/) — no signup needed. 2. [Create a Webhook Relay account](https://my.webhookrelay.com/register), install the agent, and run `relay forward --bucket square http://localhost:8080/webhook`. 3. Point your Square subscription's Notification URL at the stable endpoint, click Send Test Event, and watch it hit `localhost`. You will be testing real Square events against your local handler in a few minutes — no deploys, no open firewall ports, and a URL you configure exactly once. --- --- title: Kubernetes Operator | WebhookRelay meta: "og: title": "Kubernetes Operator" description: Trigger Jenkins builds on push to Github using Webhook Relay Operator url: https://webhookrelay.com/docs/tutorials/cicd/kubernetes-operator.md file: /docs/tutorials/cicd/kubernetes-operator.md --- ![Stripes](https://webhookrelay.com/docs/tutorials/cicd/kubernetes-operator/images/stripes.svg) Documentation **Fundamentals** # **Kubernetes Operator** Trigger Jenkins builds on push to Github using Webhook Relay Operator In this tutorial, we will configure a Jenkins pipeline on Kubernetes that leverages Jenkins and Webhook Relay operators. Jenkins Kubernetes operator will be creating Jenkins instances with a predefined seed job. Webhook Relay operator will ensure that GitHub webhooks on push events trigger new Jenkins builds for a fast and efficient CI/CD experience. ![Webhook Relay and Jenkins Operators](https://webhookrelay.com/docs/tutorials/cicd/kubernetes-operator/images/tutorials/jenkins-operator/operator.png) Advantages of this setup: - Your Jenkins instance is only accessible through kubectl port-forward while maintaining the ability to receive webhooks from public destinations. - Jenkins pipeline configuration is stored in Git. - Webhook Relay routing configuration is stored in Git, the same as the Jenkins itself. You can read about operator pattern in [Kubernetes docs](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/). ## [Prerequisites](#prerequisites) Prerequisites: - [Helm](https://docs.helm.sh/using_helm/#installing-helm) - [Webhook Relay account](https://my.webhookrelay.com) - [Kubernetes](https://kubernetes.io/) environment, Minikube, k3s, GKE, AKS, etc. are fine. - Configured kubectl - Git ## [Installation](#installation) The installation will consist of several steps: - Installing Jenkins operator - Installing Webhook Relay operator ## [Create a fresh namespace](#create-a-fresh-namespace) Let's start by creating a new namespace where we will put our Jenkins instance and run builds. I will call it 'jenkins' but you can choose any other name: ``` kubectl create namespace jenkins ``` And then switch to it: ``` kubectl config set-context $(kubectl config current-context) --namespace=jenkins ``` ## [Jenkins Operator](#jenkins-operator) We will install Jenkins operator using Helm. First, add the repository: ``` helm repo add jenkins https://raw.githubusercontent.com/jenkinsci/kubernetes-operator/master/chart helm repo update ``` Once the repository has been added, install it: ``` helm install jenkins-operator jenkins/jenkins-operator ``` Official docs can be found here: [https://jenkinsci.github.io/kubernetes-operator/docs/installation/](https://jenkinsci.github.io/kubernetes-operator/docs/installation/). The operator is not the Jenkins itself so to get our Jenkins instance, we will have to create a [Custom Resource](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/). > Custom resources are extensions of the Kubernetes API. This page discusses when to add a custom resource to your Kubernetes cluster and when to use a standalone service. It describes the two methods for adding custom resources and how to choose between them. ## [Start Jenkins (using Custom Resource)](#start-jenkins-using-custom-resource) We will need to create a CR. You can either use Jenkins Operator docs to create one or you can fork this [https://github.com/webhookrelay/jenkins-operator-example.git](https://github.com/webhookrelay/jenkins-operator-example.git) repository and clone it. Then: 1. Update **jenkins_cr.yaml** [file](https://github.com/webhookrelay/jenkins-operator-example/blob/master/jenkins_cr.yaml) `https://github.com/webhookrelay/jenkins-operator-example.git` to your own repository fork (it will usually be `https://github.com//jenkins-operator-example.git`). 2. Create it with kubectl: ``` kubectl apply -f jenkins_cr.yaml ``` Main differences in this file from the stock example: - Added `github` plugin as we will need it to trigger jobs - Seed job got `githubPushTrigger: true` set as well Creating this PR should result in two additional containers: ``` kubectl get pods NAME READY STATUS RESTARTS AGE jenkins-jenkins 1/1 Running 0 7m11s jenkins-operator-6dbbc458c9-gmx6p 1/1 Running 0 18m seed-job-agent-jenkins-65cc4bc684-9ztr5 1/1 Running 0 6m21s ``` Let's connect to Jenkins. First, get username and password: ``` kubectl --namespace jenkins get secret jenkins-operator-credentials-jenkins -o 'jsonpath={.data.user}' | base64 -d kubectl --namespace jenkins get secret jenkins-operator-credentials-jenkins -o 'jsonpath={.data.password}' | base64 -d ``` Then, in one terminal start port forwarding: ``` kubectl port-forward jenkins-jenkins 8080:8080 ``` And then just open [http://localhost:8080](http://localhost:8080) in your browser. ![Jenkins dashboard](https://webhookrelay.com/docs/tutorials/cicd/kubernetes-operator/images/tutorials/jenkins-operator/jenkins.png) ## [Webhook Relay](#webhook-relay) Retrieve your access token key & secret pair from [https://my.webhookrelay.com/tokens](https://my.webhookrelay.com/tokens) and set them as an environment variables: ``` export RELAY_KEY=xxxxxxxxxxxx export RELAY_SECRET=xxxxx ``` Add Webhook Relay Operator Helm repository and install it: ``` helm repo add webhookrelay https://charts.webhookrelay.com helm repo update helm upgrade --install webhookrelay-operator --namespace=jenkins webhookrelay/webhookrelay-operator \ --set credentials.key=$RELAY_KEY --set credentials.secret=$RELAY_SECRET ``` > Operator doesn't forward webhooks on its own. Each created CR will ensure an agent deployment that is configured to route specific buckets. From the [operator example repository](https://github.com/webhookrelay/jenkins-operator-example/blob/master/webhookrelay_cr.yaml) we will need to create Webhook Relay Custom Resource: ``` kubectl apply -f webhookrelay_cr.yaml ``` > Note that if you have modified Jenkins CR name you will need to update webhookrelay_cr.yaml "destination" field from `destination: http://jenkins-operator-http-jenkins:8080/github-webhook/` to whatever your current Jenkins service is. Typically it will be in a format `jenkins-operator-http-`. ## [GitHub Configuration](#github-configuration) This step could be automated by making Jenkins automatically configure Github repositories to forwarding to this endpoint, however for simplicity and so that it's more clear how it works, we will add this URL manually. ### [Get your Webhook Relay public URL](#get-your-webhook-relay-public-url) To get your public endpoint you can either visit [https://my.webhookrelay.com/buckets](https://my.webhookrelay.com/buckets) page or get it via CR status: ``` kubectl get webhookrelayforwards.forward.webhookrelay.com forward-to-jenkins -o 'jsonpath={.status.publicEndpoints[0]}' ``` Result should look something like: ``` $ kubectl get webhookrelayforwards.forward.webhookrelay.com forward-to-jenkins -o 'jsonpath={.status.publicEndpoints[0]}' https://k0yv9ip5sxxp55ncsu936k.hooks.webhookrelay.com ``` ### [Add public URL to GitHub repository settings](#add-public-url-to-github-repository-settings) Take the public endpoint URL and add it to your GitHub repository: ![GitHub configuration](https://webhookrelay.com/docs/tutorials/cicd/kubernetes-operator/images/tutorials/jenkins-operator/github-config.png) ## [Using the pipeline](#using-the-pipeline) First, when the pipeline is created, trigger the build manually. After that, any push to your GitHub repository will send a webhook through Webhook Relay to your Jenkins instance that's running inside a Kubernetes cluster: ![Jenkins pipeline](https://webhookrelay.com/docs/tutorials/cicd/kubernetes-operator/images/tutorials/jenkins-operator/pipeline.png) Did this page help you? --- --- title: Test Sentry Webhooks Locally | WebhookRelay meta: "og: title": "Test Sentry Webhooks Locally" description: Test Sentry webhooks on localhost without deploying. Inspect the real payload, forward to your handler, and verify the Sentry-Hook-Signature. url: https://webhookrelay.com/blog/receive-sentry-webhooks-locally.md file: /blog/receive-sentry-webhooks-locally.md --- ![Stripes](https://webhookrelay.com/blog/receive-sentry-webhooks-locally/images/stripes.svg) # **Test Sentry Webhooks Locally** Test Sentry webhooks on localhost without deploying. Inspect the real payload, forward to your handler, and verify the Sentry-Hook-Signature. ![Test Sentry Webhooks Locally (Receive Sentry Webhooks on localhost)](https://webhookrelay.com/blog/receive-sentry-webhooks-locally/images/blog/heroes/localhost.jpg) Sentry will only deliver webhooks to a public URL. If you're building an integration — say, a small service that opens Jira tickets from new Sentry issues, or pages someone when a metric alert fires — that's the first wall you hit, because the handler you're actually working on runs on `localhost:8080` and Sentry can't see it. The workarounds people reach for are all bad in their own way. Deploying to staging on every change turns a ten-second edit into a five-minute wait. Copying a sample payload from the docs into curl feels productive, but the sample is a guess: it goes stale, and it carries none of the signature headers, so the verification code you care most about never gets exercised. Here is the setup I use to test Sentry webhooks locally with real events. It takes about ten minutes. ## [Which Sentry webhook mechanism are you on?](#which-sentry-webhook-mechanism-are-you-on) Sentry has grown three ways of sending webhooks, and they behave differently, so pin down which one you're using before wiring anything up: - Integration webhooks. Create an Internal or Public integration under Settings → Integrations → Custom Integrations and give it a Webhook URL. You get structured JSON for `installation`, `issue`, `error`, `event_alert` and `metric_alert` resources, and the requests are signed. This is the one to use. - Alert rule webhook actions. Inside an issue or metric alert rule you can add a webhook action that posts to an integration. Those payloads arrive with an `"action": "triggered"` shape wrapping the event data. - Legacy Webhooks, the old per-project plugin. It still works, but the requests aren't signed, and Sentry itself steers new setups away from it. Everything below assumes an Internal Integration, since that's what produces the `Sentry-Hook-Signature` and `Sentry-Hook-Resource` headers. ## [Step 1: look at what Sentry actually sends](#step-1-look-at-what-sentry-actually-sends) Before writing handler code, capture a real request. Open a free [Webhook Bin](https://webhookrelay.com/blog/receive-sentry-webhooks-locally/webhook-bin/) (no signup), copy the bin URL, and paste it into the Webhook URL field of a new integration: Settings → Integrations → Create New Integration → Internal Integration. Enable the resources you care about, save, then throw a test error from your app or resolve an existing issue. The request shows up in the bin within a second or two. Here's a trimmed `issue` webhook from a test project: ``` { "action": "created", "installation": { "uuid": "a8e5d37a-696c-4c54-adb5-b3f28d64c7de" }, "data": { "issue": { "id": "1170820242", "shortId": "PYTHON-E", "title": "ZeroDivisionError: division by zero", "culprit": "api.views in get", "level": "error", "status": "unresolved", "project": { "id": 1, "slug": "python" } } }, "actor": { "type": "application", "id": "sentry", "name": "Sentry" } } ``` The headers matter as much as the body: ``` Content-Type: application/json Request-ID: 3e885ff0f5f84e0eb2c4f96b1a3a4e5f Sentry-Hook-Resource: issue Sentry-Hook-Timestamp: 1749472405 Sentry-Hook-Signature: 8676dd2f8c78f7dbb9d129b9f2a879ee1a4d4a771261813e17d2dbaee7e2acb1 ``` `Sentry-Hook-Resource` tells you which resource fired, and your handler should branch on it, because the body shape differs for each one. An issue webhook wraps `data.issue` with actions like `created` or `resolved`; an alert-rule webhook puts the event under `data` with `"action": "triggered"`. If webhooks are new territory, [What is a webhook](https://webhookrelay.com/blog/receive-sentry-webhooks-locally/blog/what-is-webhook/) and [How to test webhooks](https://webhookrelay.com/blog/receive-sentry-webhooks-locally/blog/how-to-test-webhooks/) cover the ground rules. ## [Step 2: receive Sentry webhooks on localhost](#step-2-receive-sentry-webhooks-on-localhost) Once you know the shape, point the same events at your local code. [Sign up for Webhook Relay](https://my.webhookrelay.com/register), create a bucket (mine is called `sentry`), and start the agent: ``` relay forward --bucket sentry http://localhost:8080/webhook ``` The bucket has a public input URL that never changes. Set it as the integration's Webhook URL and you won't touch the Sentry config again — restart the agent, reboot, come back next week, the endpoint is still there. The agent holds an outbound connection to Webhook Relay and streams incoming requests down to `localhost:8080`, so nothing needs a public IP and no firewall port opens. It works the same from a laptop behind a corporate proxy, and the `webhookrelay/webhookrelayd` Docker image runs the same forwarding if your handler lives in a container. Details are in the [localhost forwarding docs](https://webhookrelay.com/blog/receive-sentry-webhooks-locally/docs/webhooks/internal/localhost/). Trigger another error and watch it land on your local handler. ## [Sentry quirks worth knowing](#sentry-quirks-worth-knowing) - Enabling the `issue` or `error` resource means a webhook fires on every issue create, resolve, assign and archive across the organization — noisy on a busy org. If you only want events from specific conditions, leave the resource toggles off and attach a webhook action to an alert rule instead. - Installing an internal integration immediately fires an `installation` webhook. That's a convenient reachability check before you trigger any real errors. - The integration's Client Secret is what signs every request. You'll need it for verification below. - The legacy plugin lives somewhere else entirely: Project Settings → Legacy Integrations. If your requests arrive without a `Sentry-Hook-Signature` header, you're probably on the legacy path. ## [Step 3: verify the signature](#step-3-verify-the-signature) Sentry computes an HMAC-SHA256 over the raw request body with your Client Secret and sends the hex digest in `Sentry-Hook-Signature`. Recompute it and compare before trusting anything: ``` const crypto = require("crypto"); function validSentrySignature(rawBody, signatureHeader, clientSecret) { const digest = crypto .createHmac("sha256", clientSecret) .update(rawBody, "utf8") .digest("hex"); return ( digest.length === signatureHeader.length && crypto.timingSafeEqual(Buffer.from(digest), Buffer.from(signatureHeader)) ); } ``` The classic mistake here is computing the HMAC over a re-serialized body after your framework's JSON middleware has already parsed it. Key order shifts, the bytes change, and the digest never matches — hash the raw bytes you received. If the digest still disagrees with the header, paste the captured body, your Client Secret and the received signature into the free [HMAC signature verifier](https://webhookrelay.com/blog/receive-sentry-webhooks-locally/hmac-verification/) to see which side is wrong. Language-specific examples and the other common pitfalls are in [Verify a webhook signature](https://webhookrelay.com/blog/receive-sentry-webhooks-locally/blog/verify-webhook-signature/). ## [Replay instead of re-triggering](#replay-instead-of-re-triggering) Requests that pass through the bucket are stored, so when the handler misbehaves you can replay the same delivery from the Webhook Relay dashboard rather than causing another error in your app. Edit code, replay, check the result, repeat. No commits or deploys involved. In practice this is where most of the time savings come from: the forwarding gets Sentry to your laptop, but replay is what makes the iteration loop fast. ## [Get started](#get-started) 1. Capture a real payload in a free [Webhook Bin](https://webhookrelay.com/blog/receive-sentry-webhooks-locally/webhook-bin/). 2. [Create a Webhook Relay account](https://my.webhookrelay.com/register) and run `relay forward --bucket sentry http://localhost:8080/webhook`. 3. Point your Sentry integration's Webhook URL at the bucket endpoint and trigger an error. From there you're testing live Sentry events against local code, with a webhook URL you configure exactly once. --- --- title: Receive Auth0 Webhooks Locally | WebhookRelay meta: "og: title": "Receive Auth0 Webhooks Locally" description: Test Auth0 webhooks locally by streaming Auth0 Log Streams to localhost. Inspect the real batch of log events, forward to your handler, and verify the token. url: https://webhookrelay.com/blog/receive-auth0-webhooks-locally.md file: /blog/receive-auth0-webhooks-locally.md --- ![Stripes](https://webhookrelay.com/blog/receive-auth0-webhooks-locally/images/stripes.svg) # **Receive Auth0 Webhooks Locally** Test Auth0 webhooks locally by streaming Auth0 Log Streams to localhost. Inspect the real batch of log events, forward to your handler, and verify the token. ![Receive Auth0 Webhooks Locally (Test Auth0 Log Streams on localhost)](https://webhookrelay.com/blog/receive-auth0-webhooks-locally/images/blog/heroes/localhost.jpg) You are wiring up an Auth0 integration — shipping login events to your own analytics, reacting to failed logins, or syncing user activity — and you need to see your handler react to a real batch of Auth0 log events. The problem is immediate: Auth0 delivers these events through a **Log Stream** that will only POST to a **public URL**, and your handler is running on `localhost:8080`. Auth0 has no way to reach it. The usual workarounds are painful. Deploying to a staging server for every code change is slow. Pasting a sample event from the docs into curl gives you a _guess_ at the real request, not the real headers and the real batched array Auth0 actually sends. What you want is to **receive Auth0 webhooks locally** — real log events, hitting your local handler, with a URL that does not change every time you restart. This guide shows how to do exactly that. ## [Why receiving Auth0 webhooks locally is tricky](#why-receiving-auth0-webhooks-locally-is-tricky) Auth0's main webhook mechanism is **Log Streams**. You create a _Custom Webhook_ stream, and Auth0 batches your tenant's log events (logins, signups, token exchanges, failed logins, Management API calls) and POSTs them to a URL as JSON. The catch is the same as with any provider: Auth0 sits on the public internet; your dev machine usually does not. It is behind a router, a corporate firewall, or both, with no public IP and no inbound ports open. So you need something in the middle: a public endpoint Auth0 can hit, that relays each request down to your laptop without you opening a single firewall port. That is what [Webhook Relay](https://my.webhookrelay.com/register) does — and unlike a random tunnel URL, the endpoint is **stable**, so you configure the Auth0 stream once and never touch it again. ## [Step 1: Inspect the real payload with Webhook Bin](#step-1-inspect-the-real-payload-with-webhook-bin) Before you write any handler code, find out what Auth0 actually sends. Open the free [Webhook Bin](https://webhookrelay.com/blog/receive-auth0-webhooks-locally/webhook-bin/) — no signup — and you get an instant public URL. 1. Copy the Webhook Bin URL. 2. In the Auth0 Dashboard, go to **Monitoring → Streams → Create Stream**. 3. Choose **Custom Webhook**, give it a name, and paste the URL into **Payload URL**. 4. Set **Content Type** to `application/json` and **Content Format** to `JSON Lines` (or `JSON Array`), then save. Now perform an action that produces a log event — sign in to your app, trigger a failed login, or call the Management API. Auth0 batches recent events and POSTs them to your endpoint. Inspect the captured request in Webhook Bin: the full JSON body (an **array of log events**, each with `type`, `date`, `client_id`, `ip`, and more), the content type, and every header — including the `Authorization` header if you set a token. Now you know the exact shape of the data — and that it arrives as a _batch_, not one event at a time — before writing a line of code. For more on this approach, see [How to test webhooks](https://webhookrelay.com/blog/receive-auth0-webhooks-locally/blog/how-to-test-webhooks/) and [What is a webhook](https://webhookrelay.com/blog/receive-auth0-webhooks-locally/blog/what-is-webhook/). ## [Step 2: Forward the events to localhost with the relay agent](#step-2-forward-the-events-to-localhost-with-the-relay-agent) Once you know the payload, route those same events into your local handler. [Sign up for Webhook Relay](https://my.webhookrelay.com/register), install the `relay` agent (CLI or Docker), and create a bucket — say `auth0`. The bucket gives you a stable public input endpoint. Start forwarding to your local server: ``` relay forward --bucket auth0 http://localhost:8080/api/logs ``` The agent opens an **outbound** connection to Webhook Relay and streams every incoming request down to `http://localhost:8080/api/logs`. Because the connection is outbound, there are **no firewall ports to open and no public IP needed** — this works from your laptop, behind a corporate proxy, or inside a Kubernetes cluster. Running in Docker? The same command works in the official `webhookrelay/webhookrelayd` image. Full details are in the [localhost forwarding docs](https://webhookrelay.com/blog/receive-auth0-webhooks-locally/docs/webhooks/internal/localhost/). Now update the Log Stream's **Payload URL** to your Webhook Relay endpoint (or just create the stream there from the start). Trigger a login and watch the batch arrive on `localhost`. ## [Auth0-specific configuration and quirks](#auth0-specific-configuration-and-quirks) A few Auth0 details worth knowing: - **Where to add it:** Auth0 Dashboard → **Monitoring → Streams → Create Stream → Custom Webhook**. One Payload URL per stream, though you can reuse the same URL across multiple streams. - **Batched delivery:** Auth0 does **not** send one event per request. Each POST body is an array (or newline-delimited objects in JSON Lines format) containing multiple log events. Loop over them in your handler. - **Content format:** `JSON Lines` delivers one JSON object per line; `JSON Array` delivers a single array. Pick the one your parser handles and be consistent. - **Single route:** your handler only needs one endpoint (such as `/api/logs`) that accepts HTTP `POST`. - **Actions, too:** beyond Log Streams, an Auth0 **Action** can call out to a custom webhook with `fetch()` during a flow (for example, post to your endpoint on every login). Those requests also need a public URL, so the same relay setup applies. - **Health view:** check the stream's **Health** tab in the dashboard to confirm Auth0 is delivering and seeing 2xx responses from your endpoint. ## [Step 3: Verify the Auth0 webhook token](#step-3-verify-the-auth0-webhook-token) A custom webhook stream lets you set an optional **Authorization Token**. When you configure one, Auth0 sends it in the **`Authorization`** header (formatted as `Bearer `) on every request. Your handler should compare the received header against the token you configured — in constant time — before trusting the payload. This is a shared-secret check rather than a per-request HMAC signature, so guard the token like a password and serve the endpoint over HTTPS. If you are integrating with a provider that signs each request with an HMAC instead, you can sanity-check that flow by pasting a captured body, your secret, and the received signature into the free [HMAC signature verifier](https://webhookrelay.com/blog/receive-auth0-webhooks-locally/hmac-verification/). For language-specific code and the common pitfalls — reading the body after a JSON parser has consumed it, timing-safe comparison — read [Verify a webhook signature](https://webhookrelay.com/blog/receive-auth0-webhooks-locally/blog/verify-webhook-signature/). ## [Replay and iterate](#replay-and-iterate) This is where local development gets fast: - **Replay from Webhook Relay** — past requests are stored on your bucket, so you can resend a captured batch of log events without producing new activity in Auth0. - **Iterate on your handler** by editing code and replaying the same delivery until it parses the array, handles each event `type`, and checks the token correctly. No commits, no pushes, no deploys just to test a code path. - **No flaky test logins** — once you have one real batch captured, you can replay it endlessly instead of repeatedly logging in to generate events. Because the Webhook Relay endpoint is stable, you can stop and restart the agent, reboot your machine, or come back next week — the Auth0 Log Stream configuration never needs to change. ## [Get started](#get-started) 1. Inspect the real payload in the free [Webhook Bin](https://webhookrelay.com/blog/receive-auth0-webhooks-locally/webhook-bin/) — no signup needed. 2. [Create a Webhook Relay account](https://my.webhookrelay.com/register), install the agent, and run `relay forward --bucket auth0 http://localhost:8080/api/logs`. 3. Point your Auth0 custom webhook stream at the stable endpoint, trigger a login, and watch the batch hit `localhost`. You will be testing real Auth0 log events against your local handler in a few minutes — no deploys, no open firewall ports, and a URL you configure exactly once. --- --- title: Test Clerk Webhooks Locally | WebhookRelay meta: "og: title": "Test Clerk Webhooks Locally" description: Test Clerk webhooks locally and receive them on localhost without deploying. Inspect the real payload, forward to your handler, and verify the Svix signature. url: https://webhookrelay.com/blog/receive-clerk-webhooks-locally.md file: /blog/receive-clerk-webhooks-locally.md --- ![Stripes](https://webhookrelay.com/blog/receive-clerk-webhooks-locally/images/stripes.svg) # **Test Clerk Webhooks Locally** Test Clerk webhooks locally and receive them on localhost without deploying. Inspect the real payload, forward to your handler, and verify the Svix signature. ![Test Clerk Webhooks Locally (Receive Clerk Webhooks on localhost)](https://webhookrelay.com/blog/receive-clerk-webhooks-locally/images/blog/heroes/localhost.jpg) You are building a Clerk integration — provisioning a user record when someone signs up, syncing profile changes, cleaning up when an account is deleted — and you need to see your handler react to a real `user.created` or `session.created` event. The problem is immediate: Clerk will only POST to a **public URL**, and your handler is running on `localhost:3000`. Clerk has no way to reach it. The usual workarounds are painful. Deploying to a staging server for every code change is slow. Copying a sample payload from the docs into curl gives you a _guess_ at the real request, not the real headers and body Clerk actually sends — and you cannot test signature verification at all without the real Svix headers. What you want is to **test Clerk webhooks locally** — real events, hitting your local handler, with a URL that does not change every time you restart. This guide shows how to do exactly that. ## [Why receiving Clerk webhooks locally is tricky](#why-receiving-clerk-webhooks-locally-is-tricky) A webhook is just an HTTP request that Clerk sends to a URL when something happens — a user is created, updated, or deleted; a session starts; an organization changes. Clerk sits on the public internet; your dev machine usually does not. It is behind a router, a corporate firewall, or both, with no public IP and no inbound ports open. So you need something in the middle: a public endpoint Clerk can hit, that relays each request down to your laptop without you opening a single firewall port. That is what [Webhook Relay](https://my.webhookrelay.com/register) does — and unlike a random tunnel URL, the endpoint is **stable**, so you configure Clerk once and never touch it again. ## [Step 1: Inspect the real payload with Webhook Bin](#step-1-inspect-the-real-payload-with-webhook-bin) Before you write any handler code, find out what Clerk actually sends. Open the free [Webhook Bin](https://webhookrelay.com/blog/receive-clerk-webhooks-locally/webhook-bin/) — no signup — and you get an instant public URL. 1. Copy the Webhook Bin URL. 2. In the [Clerk Dashboard](https://clerk.com), go to **Webhooks → Add Endpoint**. 3. Paste the URL into **Endpoint URL**, subscribe to the events you care about (for example `user.created`, `user.updated`, `session.created`), and create the endpoint. 4. Use the dashboard's **Send example** / **Testing** tab to fire a test event, or trigger a real one by signing up a user. The request lands in Webhook Bin right away. Inspect the captured request: the full JSON body (for `user.*` events the payload is the User object), and every header, including the Svix trio — `svix-id`, `svix-timestamp`, and `svix-signature`. Now you know the exact shape of the data before writing a line of code. For more on this approach, see [How to test webhooks](https://webhookrelay.com/blog/receive-clerk-webhooks-locally/blog/how-to-test-webhooks/) and [What is a webhook](https://webhookrelay.com/blog/receive-clerk-webhooks-locally/blog/what-is-webhook/). ## [Step 2: Forward the events to localhost with the relay agent](#step-2-forward-the-events-to-localhost-with-the-relay-agent) Once you know the payload, route those same events into your local handler. [Sign up for Webhook Relay](https://my.webhookrelay.com/register), install the `relay` agent (CLI or Docker), and create a bucket — say `clerk`. The bucket gives you a stable public input endpoint. Start forwarding to your local server: ``` relay forward --bucket clerk http://localhost:3000/api/webhooks/clerk ``` The agent opens an **outbound** connection to Webhook Relay and streams every incoming request down to your local route. Because the connection is outbound, there are **no firewall ports to open and no public IP needed** — this works from your laptop, behind a corporate proxy, or inside a Kubernetes cluster. Running in Docker? The same command works in the official `webhookrelay/webhookrelayd` image. Full details are in the [localhost forwarding docs](https://webhookrelay.com/blog/receive-clerk-webhooks-locally/docs/webhooks/internal/localhost/). Now set the Clerk endpoint's URL to your Webhook Relay endpoint (or just create it there from the start). Sign up a test user and watch it arrive on `localhost`. ## [Clerk-specific configuration and quirks](#clerk-specific-configuration-and-quirks) A few Clerk details worth knowing: - **Where to add it:** **Clerk Dashboard → Webhooks → Add Endpoint**. Each endpoint has its own URL and its own signing secret. - **Subscribe to events explicitly:** Clerk does not send everything by default. Choose the events you want — `user.created`, `user.updated`, `user.deleted`, `session.created`, `organization.created`, and so on. Branch on the `type` field in the body. - **The payload wraps the object:** the body has a `type` (the event name), a `data` object (the resource — for `user.*` events that is the full User object), and an `object` field. Read `data` for the entity, `type` to decide what to do. - **Signing secret:** each endpoint shows a **Signing Secret** (it starts with `whsec_`). You will need it for verification — store it as `CLERK_WEBHOOK_SIGNING_SECRET`. - **It is Svix under the hood:** Clerk uses Svix to deliver webhooks, which is why you see `svix-*` headers rather than a single Clerk-specific signature header. Svix also handles retries, so build your handler to be idempotent (the `svix-id` is a stable delivery ID you can dedupe on). ## [Step 3: Verify the Clerk webhook signature](#step-3-verify-the-clerk-webhook-signature) Clerk's webhooks are signed by Svix. Each request carries `svix-id`, `svix-timestamp`, and `svix-signature`. The signature is a **base64-encoded HMAC-SHA256** computed over the concatenation of the `svix-id`, the `svix-timestamp`, and the **raw** request body, using your endpoint's signing secret. The `svix-signature` header can contain multiple space-separated signatures, so compare against each. The strongly recommended approach is to **not** hand-roll this. Use the `svix` library's `Webhook(secret).verify(payload, headers)`, or Clerk's framework helper `verifyWebhook(req)` (which reads `CLERK_WEBHOOK_SIGNING_SECRET` automatically and throws on a bad signature). These check the timestamp window and the HMAC for you. Skipping verification — even for a notification-only handler — leaves the endpoint open to spoofed events. To understand what is happening under the hood, paste a captured body, your signing secret, and the received signature into the free [HMAC signature verifier](https://webhookrelay.com/blog/receive-clerk-webhooks-locally/hmac-verification/). For language-specific code and the common pitfalls (reading the body after a JSON parser has consumed it, the signed payload being `id.timestamp.body`, timing-safe comparison), read [Verify a webhook signature](https://webhookrelay.com/blog/receive-clerk-webhooks-locally/blog/verify-webhook-signature/). ## [Replay and iterate](#replay-and-iterate) This is where local development gets fast: - **Replay from Clerk** — the Webhooks dashboard keeps a delivery log and lets you **resend** a past message, which re-runs it against your handler. - **Replay from Webhook Relay** — past requests are stored on your bucket, so you can resend a captured event without touching Clerk at all. The `svix-*` headers are preserved, so signature verification still works on replay. - **Iterate on your handler** by editing code and replaying the same delivery until it behaves correctly. No commits, no pushes, no deploys just to test a code path. Because the Webhook Relay endpoint is stable, you can stop and restart the agent, reboot your machine, or come back next week — the Clerk configuration never needs to change. ## [Get started](#get-started) 1. Inspect the real payload in the free [Webhook Bin](https://webhookrelay.com/blog/receive-clerk-webhooks-locally/webhook-bin/) — no signup needed. 2. [Create a Webhook Relay account](https://my.webhookrelay.com/register), install the agent, and run `relay forward --bucket clerk http://localhost:3000/api/webhooks/clerk`. 3. Point your Clerk webhook endpoint at the stable URL, sign up a test user, and watch it hit `localhost`. You will be testing real Clerk events against your local handler in a few minutes — no deploys, no open firewall ports, and a URL you configure exactly once. --- --- title: Receive Vercel Webhooks Locally | WebhookRelay meta: "og: title": "Receive Vercel Webhooks Locally" description: Test Vercel webhooks and run your handler on localhost. Inspect the real deployment payload and verify the x-vercel-signature header. url: https://webhookrelay.com/blog/receive-vercel-webhooks-locally.md file: /blog/receive-vercel-webhooks-locally.md --- ![Stripes](https://webhookrelay.com/blog/receive-vercel-webhooks-locally/images/stripes.svg) # **Receive Vercel Webhooks Locally** Test Vercel webhooks and run your handler on localhost. Inspect the real deployment payload and verify the x-vercel-signature header. ![Receive Vercel Webhooks Locally (Test Vercel Webhooks on localhost)](https://webhookrelay.com/blog/receive-vercel-webhooks-locally/images/blog/heroes/localhost.jpg) You are building a Vercel integration — a Slack notifier for failed builds, a changelog updater, an internal dashboard that tracks deploys — and you need to see your handler react to a real `deployment.succeeded` or `deployment.error` event. The problem is immediate: Vercel will only POST to a **public URL**, and your handler is running on `localhost:8080`. Vercel has no way to reach it. The usual workarounds are painful. Deploying your handler for every code change is slow and ironic. Pasting a sample payload from the docs into curl gives you a _guess_ at the real request, not the real headers and body Vercel actually sends. What you want is to **receive Vercel webhooks locally** — real events, hitting your local handler, with a URL that does not change every time you restart. This guide shows how to do exactly that. ## [Why receiving Vercel webhooks locally is tricky](#why-receiving-vercel-webhooks-locally-is-tricky) A webhook is just an HTTP request Vercel sends to a URL when something happens — a deployment starts, succeeds, errors, or is canceled; a project is created. Vercel sits on the public internet; your dev machine usually does not. It is behind a router, a corporate firewall, or both, with no public IP and no inbound ports open. So you need something in the middle: a public endpoint Vercel can hit, that relays each request down to your laptop without you opening a single firewall port. That is what [Webhook Relay](https://my.webhookrelay.com/register) does — and unlike a random tunnel URL, the endpoint is **stable**, so you configure Vercel once and never touch it again. (One note before you start: webhooks are a **Pro and Enterprise** feature on Vercel, configured at the team level. Make sure your team is on a plan that includes them.) ## [Step 1: Inspect the real payload with Webhook Bin](#step-1-inspect-the-real-payload-with-webhook-bin) Before you write any handler code, find out what Vercel actually sends. Open the free [Webhook Bin](https://webhookrelay.com/blog/receive-vercel-webhooks-locally/webhook-bin/) — no signup — and you get an instant public URL. 1. Copy the Webhook Bin URL. 2. In the Vercel Dashboard, select your team scope and go to **Settings → Webhooks**. 3. Click **Create Webhook**, paste the URL as the endpoint, and select the events you care about (for example **Deployment Created**, **Deployment Succeeded**, **Deployment Error**). 4. Save the webhook. Now trigger a real action — push to a connected repo or redeploy from the dashboard. Vercel POSTs the event to your endpoint. Inspect the captured request in Webhook Bin: the full JSON body (with the event `type`, `id`, `payload`, and details about the deployment, project, and team), and every header, including the **`x-vercel-signature`** signature. Now you know the exact shape of the data before writing a line of code. For more on this approach, see [How to test webhooks](https://webhookrelay.com/blog/receive-vercel-webhooks-locally/blog/how-to-test-webhooks/) and [What is a webhook](https://webhookrelay.com/blog/receive-vercel-webhooks-locally/blog/what-is-webhook/). ## [Step 2: Forward the events to localhost with the relay agent](#step-2-forward-the-events-to-localhost-with-the-relay-agent) Once you know the payload, route those same events into your local handler. [Sign up for Webhook Relay](https://my.webhookrelay.com/register), install the `relay` agent (CLI or Docker), and create a bucket — say `vercel`. The bucket gives you a stable public input endpoint. Start forwarding to your local server: ``` relay forward --bucket vercel http://localhost:8080/webhook ``` The agent opens an **outbound** connection to Webhook Relay and streams every incoming request down to `http://localhost:8080/webhook`. Because the connection is outbound, there are **no firewall ports to open and no public IP needed** — this works from your laptop, behind a corporate proxy, or inside a Kubernetes cluster. Running in Docker? The same command works in the official `webhookrelay/webhookrelayd` image. Full details are in the [localhost forwarding docs](https://webhookrelay.com/blog/receive-vercel-webhooks-locally/docs/webhooks/internal/localhost/). Now update the Vercel webhook's endpoint to your Webhook Relay URL (or just create it there from the start). Push a change and watch the `deployment.created` and `deployment.succeeded` events arrive on `localhost`. ## [Vercel-specific configuration and quirks](#vercel-specific-configuration-and-quirks) A few Vercel details worth knowing: - **Where to add it:** select your **team scope**, then **Settings → Webhooks**. Webhooks live at the account/team level (or per Integration), not per individual project from inside a project's settings. - **Events:** common ones include `deployment.created`, `deployment.succeeded`, `deployment.error`, and `deployment.canceled`, plus project events like `project.created`. Some project-scoped events are only available when the webhook covers **all team projects**. Branch on the `type` field in your handler. - **JSON payload:** the body is JSON, posted to your endpoint as a single `POST` per event. - **The secret is shown once:** when you create a webhook, Vercel displays a **secret key once** and never again. Copy it immediately — you need it to verify signatures (see below). - **Integration webhooks:** if you are building a Vercel Integration rather than a team webhook, you verify with your **Integration (Client) Secret** instead of a per-webhook secret. - **Limits:** Vercel allows up to 20 custom webhooks per team, which is plenty for a dev endpoint plus production. ## [Step 3: Verify the Vercel webhook signature](#step-3-verify-the-vercel-webhook-signature) Vercel signs every request. It computes an **HMAC-SHA1** of the **raw request body** using your webhook secret and sends the digest, **hex-encoded**, in the **`x-vercel-signature`** header. Your handler should recompute the same HMAC over the raw body and compare in constant time before trusting the payload. In Node.js that is essentially: ``` const expected = crypto .createHmac('sha1', secret) .update(rawBody) // the raw bytes, before JSON parsing .digest('hex'); // constant-time compare expected with req.headers['x-vercel-signature'] ``` The single most common mistake is computing the HMAC over a re-serialized body after your framework has already parsed the JSON — the bytes differ and verification fails. Capture the **raw** body. To sanity-check your implementation, paste a captured body, your secret, and the received signature into the free [HMAC signature verifier](https://webhookrelay.com/blog/receive-vercel-webhooks-locally/hmac-verification/). For language-specific code and the rest of the pitfalls — raw-body handling, timing-safe comparison — read [Verify a webhook signature](https://webhookrelay.com/blog/receive-vercel-webhooks-locally/blog/verify-webhook-signature/). ## [Replay and iterate](#replay-and-iterate) This is where local development gets fast: - **Replay from Webhook Relay** — past requests are stored on your bucket, so you can resend a captured deployment event without triggering a new build on Vercel. - **Iterate on your handler** by editing code and replaying the same delivery until it branches on `type`, parses the deployment details, and verifies the SHA1 signature correctly. No new commits, no pushes, no deploys just to test a code path. - **No wasted builds** — you do not have to redeploy your project repeatedly just to generate a fresh `deployment.succeeded` event; one captured event replays as many times as you need. Because the Webhook Relay endpoint is stable, you can stop and restart the agent, reboot your machine, or come back next week — the Vercel webhook configuration never needs to change. ## [Get started](#get-started) 1. Inspect the real payload in the free [Webhook Bin](https://webhookrelay.com/blog/receive-vercel-webhooks-locally/webhook-bin/) — no signup needed. 2. [Create a Webhook Relay account](https://my.webhookrelay.com/register), install the agent, and run `relay forward --bucket vercel http://localhost:8080/webhook`. 3. Point your Vercel team webhook at the stable endpoint, trigger a deployment, and watch it hit `localhost`. You will be testing real Vercel deployment events against your local handler in a few minutes — no extra deploys, no open firewall ports, and a URL you configure exactly once. --- --- title: Test Linear Webhooks Locally | WebhookRelay meta: "og: title": "Test Linear Webhooks Locally" description: Test Linear webhooks on localhost without deploying. Inspect the real payload, forward to your handler, and verify the Linear-Signature header. url: https://webhookrelay.com/blog/receive-linear-webhooks-locally.md file: /blog/receive-linear-webhooks-locally.md --- ![Stripes](https://webhookrelay.com/blog/receive-linear-webhooks-locally/images/stripes.svg) # **Test Linear Webhooks Locally** Test Linear webhooks on localhost without deploying. Inspect the real payload, forward to your handler, and verify the Linear-Signature header. ![Test Linear Webhooks Locally (Receive Linear Webhooks on localhost)](https://webhookrelay.com/blog/receive-linear-webhooks-locally/images/blog/heroes/localhost.jpg) You are building a Linear integration — a Slack notifier, a deployment trigger, a sync to your own database — and you need to watch your handler react to a real `Issue` or `Comment` event. The problem hits immediately: Linear will only POST to a **public URL**, and your handler is running on `localhost:8080`. Linear has no way to reach it. The usual workarounds are slow. Deploying to a staging environment for every code change kills your iteration speed. Copying a sample payload out of the docs into curl gives you a _guess_ at the real request, not the exact headers and body Linear actually sends. What you really want is to **test Linear webhooks locally** — real events, hitting your local handler, on a URL that does not change every time you restart. This guide shows how to do exactly that. ## [Why testing Linear webhooks locally is tricky](#why-testing-linear-webhooks-locally-is-tricky) A webhook is just an HTTP request that Linear sends to a URL when something changes in your workspace — an issue is created, a comment is added, a project is updated. Linear lives on the public internet; your dev machine usually does not. It sits behind a router, a corporate firewall, or both, with no public IP and no inbound ports open. So you need something in the middle: a public endpoint Linear can hit that relays each request down to your laptop without you opening a single firewall port. That is what [Webhook Relay](https://my.webhookrelay.com/register) does — and unlike a random tunnel URL, the endpoint is **stable**, so you configure Linear once and never touch it again. ## [Step 1: Inspect the real payload with Webhook Bin](#step-1-inspect-the-real-payload-with-webhook-bin) Before you write any handler code, find out what Linear actually sends. Open the free [Webhook Bin](https://webhookrelay.com/blog/receive-linear-webhooks-locally/webhook-bin/) — no signup — and you get an instant public URL. 1. Copy the Webhook Bin URL. 2. In Linear, go to **Settings → API → Webhooks** and click **New webhook** (you can also create a webhook scoped to a single team). 3. Paste the URL into the webhook **URL** field. 4. Choose the resource events you care about — **Issues**, **Comments**, **Projects**, **Issue labels**, and more — then save. Trigger a real action (create an issue, drop a comment) and inspect the captured request: the full JSON body, including the `action` (`create`, `update`, `remove`), the `type`, the `data` object, and the `webhookTimestamp`. You will also see every header, including `Linear-Event`, `Linear-Delivery`, and `Linear-Signature`. Now you know the exact shape of the data before writing a line of code. For more on this approach, see [How to test webhooks](https://webhookrelay.com/blog/receive-linear-webhooks-locally/blog/how-to-test-webhooks/) and [What is a webhook](https://webhookrelay.com/blog/receive-linear-webhooks-locally/blog/what-is-webhook/). ## [Step 2: Forward the events to localhost with the relay agent](#step-2-forward-the-events-to-localhost-with-the-relay-agent) Once you know the payload, route those same events into your local handler. [Sign up for Webhook Relay](https://my.webhookrelay.com/register), install the `relay` agent (CLI or Docker), and create a bucket — say `linear`. The bucket gives you a stable public input endpoint. Start forwarding to your local server: ``` relay forward --bucket linear http://localhost:8080/webhook ``` The agent opens an **outbound** connection to Webhook Relay and streams every incoming request down to `http://localhost:8080/webhook`. Because the connection is outbound, there are **no firewall ports to open and no public IP needed** — this works from your laptop, behind a corporate proxy, or inside a Kubernetes cluster. Running in Docker? The same command works in the official `webhookrelay/webhookrelayd` image. Full details are in the [localhost forwarding docs](https://webhookrelay.com/blog/receive-linear-webhooks-locally/docs/webhooks/internal/localhost/). Now update the Linear webhook's **URL** to your Webhook Relay endpoint (or just create it there from the start). Create an issue and watch it arrive on `localhost`. ## [Linear-specific configuration and quirks](#linear-specific-configuration-and-quirks) A few Linear details worth knowing: - **Where to add it:** **Settings → API → Webhooks** for a workspace-level webhook, or scope a webhook to an individual team if you only care about that team's activity. - **Events:** subscribe to the resource types you need — Issues, Comments, Projects, Cycles, Issue labels, Attachments, and others. Each delivery carries the resource name in the `Linear-Event` header and an `action` field in the body, so you branch on those in your handler. - **Payload shape:** the body is always JSON. Expect top-level `action`, `type`, `data`, `url`, and `webhookTimestamp` keys. The `data` object mirrors the resource that changed. - **Per-webhook secret:** when you create the webhook, Linear shows a **signing secret** unique to that webhook. Save it — you need it to verify signatures (next step). - **Respond fast:** Linear expects a quick `2xx`. Acknowledge the delivery first, then do slow work asynchronously so you do not trip delivery timeouts. ## [Step 3: Verify the Linear webhook signature](#step-3-verify-the-linear-webhook-signature) Linear signs every request. It computes an **HMAC-SHA256** of the **raw** request body using your webhook's secret, hex-encodes the result, and sends it in the **`Linear-Signature`** header. Your handler should recompute the HMAC over the raw body and compare it to the header value in constant time before trusting the payload. Linear also includes a **`webhookTimestamp`** in the body. After the signature checks out, confirm that timestamp is recent (for example, within 60 seconds of now) and reject anything older — this stops an attacker from replaying a previously valid request. To sanity-check your implementation, paste a captured body, your secret, and the received signature into the free [HMAC signature verifier](https://webhookrelay.com/blog/receive-linear-webhooks-locally/hmac-verification/). For language-specific code and the common pitfalls (reading the body _after_ a JSON parser has already consumed it, timing-safe comparison), read [Verify a webhook signature](https://webhookrelay.com/blog/receive-linear-webhooks-locally/blog/verify-webhook-signature/). ## [Replay and iterate](#replay-and-iterate) This is where local development gets fast: - **Replay from Webhook Relay** — past requests are stored on your bucket, so you can resend a captured event against your handler without touching Linear at all. - **Iterate on your handler** by editing code and replaying the same delivery until it behaves correctly. No commits, no pushes, no deploys just to test a single code path. - **Keep `relay forward` running** while you work — events stream straight to `localhost` as you trigger them in Linear. Because the Webhook Relay endpoint is stable, you can stop and restart the agent, reboot your machine, or come back next week — the Linear configuration never needs to change. ## [Get started](#get-started) 1. Inspect the real payload in the free [Webhook Bin](https://webhookrelay.com/blog/receive-linear-webhooks-locally/webhook-bin/) — no signup needed. 2. [Create a Webhook Relay account](https://my.webhookrelay.com/register), install the agent, and run `relay forward --bucket linear http://localhost:8080/webhook`. 3. Point your Linear webhook at the stable endpoint, create an issue, and watch it hit `localhost`. You will be testing real Linear events against your local handler in a few minutes — no deploys, no open firewall ports, and a URL you configure exactly once. --- --- title: Test Typeform Webhooks Locally | WebhookRelay meta: "og: title": "Test Typeform Webhooks Locally" description: Test Typeform webhooks on localhost without deploying. Add the webhook in Typeform, inspect the real form_response payload, and verify the signature. url: https://webhookrelay.com/blog/receive-typeform-webhooks-locally.md file: /blog/receive-typeform-webhooks-locally.md --- ![Stripes](https://webhookrelay.com/blog/receive-typeform-webhooks-locally/images/stripes.svg) # **Test Typeform Webhooks Locally** Test Typeform webhooks on localhost without deploying. Add the webhook in Typeform, inspect the real form_response payload, and verify the signature. ![Test Typeform Webhooks Locally (Typeform Webhook on localhost)](https://webhookrelay.com/blog/receive-typeform-webhooks-locally/images/blog/heroes/localhost.jpg) You are building a Typeform integration — piping survey answers into your database, triggering a follow-up email, creating a lead in your CRM — and you need to see your handler react to a real form submission. The problem is immediate: Typeform will only POST to a **public URL**, and your handler is running on `localhost:8080`. Typeform has no way to reach it. The usual workarounds are painful. Deploying to a staging server for every code change is slow. Pasting the sample payload from the docs into curl gives you a _guess_ at the real request, not the real headers and body Typeform actually sends. What you want is to **test Typeform webhooks locally** — real `form_response` events, hitting your local handler, with a URL that does not change every time you restart. This guide shows how to do exactly that. ## [Why testing Typeform webhooks locally is tricky](#why-testing-typeform-webhooks-locally-is-tricky) A webhook is just an HTTP request that Typeform sends to a URL when someone submits a form. Typeform sits on the public internet; your dev machine usually does not. It is behind a router, a corporate firewall, or both, with no public IP and no inbound ports open. So you need something in the middle: a public endpoint Typeform can hit, that relays each request down to your laptop without you opening a single firewall port. That is what [Webhook Relay](https://my.webhookrelay.com/register) does — and unlike a random tunnel URL, the endpoint is **stable**, so you configure Typeform once and never touch it again. ## [Step 1: Inspect the real payload with Webhook Bin](#step-1-inspect-the-real-payload-with-webhook-bin) Before you write any handler code, find out what Typeform actually sends. Open the free [Webhook Bin](https://webhookrelay.com/blog/receive-typeform-webhooks-locally/webhook-bin/) — no signup — and you get an instant public URL. 1. Copy the Webhook Bin URL. 2. Open your form, click **Connect** in the top menu, and open the **Webhooks** tab. 3. Click **Add a webhook** and paste the URL as your endpoint. 4. Click **View deliveries**, then **Send test request** to fire a sample payload, or just submit the form yourself. Inspect the captured request in Webhook Bin: the full JSON body, the query string, and every header. Typeform wraps each submission in an `event_type` of `form_response` and a `form_response` object containing the `form_id`, a unique `token`, `submitted_at` / `landed_at` timestamps, hidden fields, the form `definition` (the questions), and the `answers` array. Test deliveries are flagged so you can tell them apart from real submissions. Now you know the exact shape of the data before writing a line of code. For more on this approach, see [How to test webhooks](https://webhookrelay.com/blog/receive-typeform-webhooks-locally/blog/how-to-test-webhooks/) and [What is a webhook](https://webhookrelay.com/blog/receive-typeform-webhooks-locally/blog/what-is-webhook/). ## [Step 2: Forward the events to localhost with the relay agent](#step-2-forward-the-events-to-localhost-with-the-relay-agent) Once you know the payload, route those same events into your local handler. [Sign up for Webhook Relay](https://my.webhookrelay.com/register), install the `relay` agent (CLI or Docker), and create a bucket — say `typeform`. The bucket gives you a stable public input endpoint. Start forwarding to your local server: ``` relay forward --bucket typeform http://localhost:8080/webhook ``` The agent opens an **outbound** connection to Webhook Relay and streams every incoming request down to `http://localhost:8080/webhook`. Because the connection is outbound, there are **no firewall ports to open and no public IP needed** — this works from your laptop, behind a corporate proxy, or inside a Kubernetes cluster. Running in Docker? The same command works in the official `webhookrelay/webhookrelayd` image. Full details are in the [localhost forwarding docs](https://webhookrelay.com/blog/receive-typeform-webhooks-locally/docs/webhooks/internal/localhost/). Now update the Typeform webhook's endpoint to your Webhook Relay URL (or just create it there from the start), and click **View deliveries → Send test request**. Watch it arrive on `localhost`. ## [Typeform-specific configuration and quirks](#typeform-specific-configuration-and-quirks) A few Typeform details worth knowing: - **Where to add it:** webhooks are configured **per form** under **Connect → Webhooks**, or programmatically via the **Webhooks API**. There is no account-wide webhook — each form has its own. - **Fixed payload:** the JSON shape is fixed. You cannot add, remove, or reshape fields before delivery, so plan your handler around the standard `form_response` structure. (If you need a different shape, transform it in Webhook Relay or your handler.) - **Partial submissions:** if someone lands on the form but does not finish, the `answers` array can be `null` with no hidden fields or score — handle that case. - **Test request:** the **View deliveries** panel has a **Send test request** button. Keep your webhook toggled **On** to redeliver. All test deliveries are clearly marked as tests. - **Secret:** add a secret via the **Edit** option to enable signed requests. ## [Step 3: Verify the Typeform webhook signature](#step-3-verify-the-typeform-webhook-signature) If you set a **secret** on the webhook, Typeform signs each request. It computes an HMAC-SHA256 over the **raw request body** using your secret, base64-encodes the result, and sends it in the **`Typeform-Signature`** header prefixed with `sha256=`. Your handler should recompute the HMAC over the **raw** body, base64-encode it, prepend `sha256=`, and compare in constant time before trusting the payload. One gotcha trips up almost everyone: you must hash the **exact raw bytes** Typeform sent. Re-serializing the JSON after parsing it changes whitespace and breaks the comparison, so capture the body before any JSON middleware touches it. To sanity-check your implementation, paste a captured body, your secret, and the received signature into the free [HMAC signature verifier](https://webhookrelay.com/blog/receive-typeform-webhooks-locally/hmac-verification/). For language-specific code and the common pitfalls (reading the body after a JSON parser has consumed it, timing-safe comparison), read [Verify a webhook signature](https://webhookrelay.com/blog/receive-typeform-webhooks-locally/blog/verify-webhook-signature/) and the [HMAC auth docs](https://webhookrelay.com/blog/receive-typeform-webhooks-locally/docs/webhooks/auth/hmac/). ## [Replay and iterate](#replay-and-iterate) This is where local development gets fast: - **Replay from Typeform** via **View deliveries → Send test request** to re-fire a delivery against your handler. - **Replay from Webhook Relay** — past requests are stored on your bucket, so you can resend a captured `form_response` without touching Typeform at all. - **Iterate on your handler** by editing code and replaying the same delivery until it behaves correctly. No commits, no pushes, no deploys just to test a code path. Because the Webhook Relay endpoint is stable, you can stop and restart the agent, reboot your machine, or come back next week — the Typeform configuration never needs to change. ## [Get started](#get-started) 1. Inspect the real payload in the free [Webhook Bin](https://webhookrelay.com/blog/receive-typeform-webhooks-locally/webhook-bin/) — no signup needed. 2. [Create a Webhook Relay account](https://my.webhookrelay.com/register), install the agent, and run `relay forward --bucket typeform http://localhost:8080/webhook`. 3. Point your Typeform webhook at the stable endpoint, send a test request, and watch it hit `localhost`. You will be testing real Typeform submissions against your local handler in a few minutes — no deploys, no open firewall ports, and a URL you configure exactly once. --- --- title: Test Calendly Webhooks Locally | WebhookRelay meta: "og: title": "Test Calendly Webhooks Locally" description: Test Calendly webhooks on localhost without deploying. Create the subscription, inspect the real invitee.created payload, and verify the signature. url: https://webhookrelay.com/blog/receive-calendly-webhooks-locally.md file: /blog/receive-calendly-webhooks-locally.md --- ![Stripes](https://webhookrelay.com/blog/receive-calendly-webhooks-locally/images/stripes.svg) # **Test Calendly Webhooks Locally** Test Calendly webhooks on localhost without deploying. Create the subscription, inspect the real invitee.created payload, and verify the signature. ![Test Calendly Webhooks Locally (Calendly Webhook on localhost)](https://webhookrelay.com/blog/receive-calendly-webhooks-locally/images/blog/heroes/localhost.jpg) You are building a Calendly integration — syncing bookings into your CRM, kicking off an onboarding flow, posting to Slack when someone schedules — and you need to see your handler react to a real `invitee.created` event. The problem is immediate: Calendly will only POST to a **public URL**, and your handler is running on `localhost:8080`. Calendly has no way to reach it. The usual workarounds are painful. Deploying to a staging server for every code change is slow. Pasting a sample payload from the docs into curl gives you a _guess_ at the real request, not the real headers and body Calendly actually sends. What you want is to **test Calendly webhooks locally** — real booking events, hitting your local handler, with a URL that does not change every time you restart. This guide shows how to do exactly that. ## [Why testing Calendly webhooks locally is tricky](#why-testing-calendly-webhooks-locally-is-tricky) A webhook is just an HTTP request that Calendly sends to a URL when someone schedules or cancels a meeting. Calendly sits on the public internet; your dev machine usually does not. It is behind a router, a corporate firewall, or both, with no public IP and no inbound ports open. So you need something in the middle: a public endpoint Calendly can hit, that relays each request down to your laptop without you opening a single firewall port. That is what [Webhook Relay](https://my.webhookrelay.com/register) does — and unlike a random tunnel URL, the endpoint is **stable**, so you create the subscription once and never touch it again. One Calendly detail to know up front: **webhook subscriptions require a paid Calendly plan**, and for most plans there is no dashboard form. You create the subscription through the **Calendly API**, not a UI toggle. ## [Step 1: Inspect the real payload with Webhook Bin](#step-1-inspect-the-real-payload-with-webhook-bin) Before you write any handler code, find out what Calendly actually sends. Open the free [Webhook Bin](https://webhookrelay.com/blog/receive-calendly-webhooks-locally/webhook-bin/) — no signup — and you get an instant public URL. 1. Copy the Webhook Bin URL. 2. Create a webhook subscription pointed at it. Calendly's subscriptions are API-created: POST to `https://api.calendly.com/webhook_subscriptions` with your **personal access token** in the `Authorization` header. ``` curl -X POST https://api.calendly.com/webhook_subscriptions \ -H "Authorization: Bearer $CALENDLY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "url": "https://YOUR-WEBHOOK-BIN-URL", "events": ["invitee.created", "invitee.canceled"], "organization": "https://api.calendly.com/organizations/AAAA", "scope": "organization" }' ``` Now book a test meeting on one of your event types (and cancel it) and inspect the captured request in Webhook Bin: the full JSON body, the query string, and every header, including `Calendly-Webhook-Signature`. The body wraps the booking under an `event` field (`invitee.created` / `invitee.canceled`) with a `payload` object containing the invitee's name, email, scheduled time, and answers to your booking questions. Now you know the exact shape of the data before writing a line of code. For more on this approach, see [How to test webhooks](https://webhookrelay.com/blog/receive-calendly-webhooks-locally/blog/how-to-test-webhooks/) and [What is a webhook](https://webhookrelay.com/blog/receive-calendly-webhooks-locally/blog/what-is-webhook/). ## [Step 2: Forward the events to localhost with the relay agent](#step-2-forward-the-events-to-localhost-with-the-relay-agent) Once you know the payload, route those same events into your local handler. [Sign up for Webhook Relay](https://my.webhookrelay.com/register), install the `relay` agent (CLI or Docker), and create a bucket — say `calendly`. The bucket gives you a stable public input endpoint. Start forwarding to your local server: ``` relay forward --bucket calendly http://localhost:8080/webhook ``` The agent opens an **outbound** connection to Webhook Relay and streams every incoming request down to `http://localhost:8080/webhook`. Because the connection is outbound, there are **no firewall ports to open and no public IP needed** — this works from your laptop, behind a corporate proxy, or inside a Kubernetes cluster. Running in Docker? The same command works in the official `webhookrelay/webhookrelayd` image. Full details are in the [localhost forwarding docs](https://webhookrelay.com/blog/receive-calendly-webhooks-locally/docs/webhooks/internal/localhost/). Now create (or update) the Calendly subscription with your **Webhook Relay endpoint** as the `url`, using the same API call as above. Schedule a test meeting and watch it arrive on `localhost`. ## [Calendly-specific configuration and quirks](#calendly-specific-configuration-and-quirks) A few Calendly details worth knowing: - **API-created, not a UI form:** for most plans there is no "Add webhook" button. You manage subscriptions via the `/webhook_subscriptions` endpoint with a personal access token (or an OAuth token). - **Events:** subscribe to `invitee.created` (scheduled) and `invitee.canceled` (canceled). A **reschedule fires both** — an `invitee.canceled` for the old slot and an `invitee.created` for the new one. Branch on the `event` field in the body. - **Scope:** set `scope` to `user` for only your own bookings, or `organization` to cover every member in your org. - **Paid plan required:** webhook subscriptions are a paid Calendly feature. - **Signing key:** the API returns a per-subscription signing key when you create the subscription — store it, you will need it to verify signatures. ## [Step 3: Verify the Calendly webhook signature](#step-3-verify-the-calendly-webhook-signature) Calendly signs every request. It computes an HMAC-SHA256 over the **timestamp plus the raw request body** using your subscription's signing key, and sends the result in the **`Calendly-Webhook-Signature`** header. The value looks like `t=1700000000,v1=abc123...` — a timestamp and a signature, the same shape Stripe uses. To verify: split the header into `t` and `v1`, build the signed string as `t + "." + rawBody`, compute HMAC-SHA256 with your signing key, and compare your digest against `v1` in constant time. The timestamp lets you reject stale requests and defend against replay attacks. To sanity-check your implementation, paste a captured body, your signing key, and the received signature into the free [HMAC signature verifier](https://webhookrelay.com/blog/receive-calendly-webhooks-locally/hmac-verification/). For language-specific code and the common pitfalls (reading the body after a JSON parser has consumed it, timing-safe comparison), read [Verify a webhook signature](https://webhookrelay.com/blog/receive-calendly-webhooks-locally/blog/verify-webhook-signature/) and the [HMAC auth docs](https://webhookrelay.com/blog/receive-calendly-webhooks-locally/docs/webhooks/auth/hmac/). ## [Replay and iterate](#replay-and-iterate) This is where local development gets fast: - **Replay from Webhook Relay** — past requests are stored on your bucket, so you can resend a captured `invitee.created` without scheduling a new meeting in Calendly every time. - **Iterate on your handler** by editing code and replaying the same delivery until it behaves correctly. No commits, no pushes, no deploys just to test a code path. Because the Webhook Relay endpoint is stable, you can stop and restart the agent, reboot your machine, or come back next week — the Calendly subscription never needs to change. ## [Get started](#get-started) 1. Inspect the real payload in the free [Webhook Bin](https://webhookrelay.com/blog/receive-calendly-webhooks-locally/webhook-bin/) — no signup needed. 2. [Create a Webhook Relay account](https://my.webhookrelay.com/register), install the agent, and run `relay forward --bucket calendly http://localhost:8080/webhook`. 3. Create a Calendly webhook subscription pointed at the stable endpoint, schedule a test meeting, and watch it hit `localhost`. You will be testing real Calendly bookings against your local handler in a few minutes — no deploys, no open firewall ports, and a URL you configure exactly once. --- --- title: Receive Mailgun Webhooks Locally | WebhookRelay meta: "og: title": "Receive Mailgun Webhooks Locally" description: Test Mailgun webhooks on localhost without deploying. Inspect the real event payload, forward delivered and bounced events, and verify the signature. url: https://webhookrelay.com/blog/receive-mailgun-webhooks-locally.md file: /blog/receive-mailgun-webhooks-locally.md --- ![Stripes](https://webhookrelay.com/blog/receive-mailgun-webhooks-locally/images/stripes.svg) # **Receive Mailgun Webhooks Locally** Test Mailgun webhooks on localhost without deploying. Inspect the real event payload, forward delivered and bounced events, and verify the signature. ![Receive Mailgun Webhooks Locally (Test Mailgun Webhooks on localhost)](https://webhookrelay.com/blog/receive-mailgun-webhooks-locally/images/blog/heroes/localhost.jpg) You are building an email integration on top of Mailgun — updating a contact's status when a message **bounces**, recording **opens** and **clicks**, flagging **complaints** — and you need to see your handler react to a real event. The problem is immediate: Mailgun will only POST to a **public URL**, and your handler is running on `localhost:8080`. Mailgun has no way to reach it. The usual workarounds are painful. Deploying to staging for every code change is slow, and pasting sample payloads from the docs into curl gives you a _guess_ at the real request — not the actual headers and JSON Mailgun sends for a `delivered` versus an `opened` event. What you want is to **receive Mailgun webhooks locally** — real events hitting your local handler, with a URL that does not change every time you restart. ## [Why testing Mailgun webhooks locally is tricky](#why-testing-mailgun-webhooks-locally-is-tricky) A Mailgun webhook is just an HTTP request that Mailgun sends to a URL when something happens to a message you sent. Mailgun sits on the public internet; your dev machine usually does not. It is behind a router, a corporate firewall, or both, with no public IP and no inbound ports open. So you need something in the middle: a public endpoint Mailgun can hit, that relays each request down to your laptop without you opening a single firewall port. That is what [Webhook Relay](https://my.webhookrelay.com/register) does — and unlike a random tunnel URL, the endpoint is **stable**, so you configure Mailgun once and never touch it again. ## [Step 1: Inspect the real payload with Webhook Bin](#step-1-inspect-the-real-payload-with-webhook-bin) Before you write any handler code, find out what Mailgun actually sends. Open the free [Webhook Bin](https://webhookrelay.com/blog/receive-mailgun-webhooks-locally/webhook-bin/) — no signup — and you get an instant public URL. 1. Copy the Webhook Bin URL. 2. In your Mailgun dashboard, go to **Sending → Webhooks**. 3. Select your sending domain, then add a webhook for an event type — for example **Delivered Messages** — and paste the Webhook Bin URL. 4. Save it. Mailgun lets you send a **test** request from the dashboard, or you can send a real message and trigger the event naturally. The captured request shows you everything: the full JSON body, the query string, and every header. A modern Mailgun payload is JSON with two top-level objects — a `signature` object (containing `timestamp`, `token`, and `signature`) and an `event-data` object (containing the `event` type, the message metadata, recipient, and timestamps). Now you know the exact shape of the data before writing a line of code. For more on this approach, see [How to test webhooks](https://webhookrelay.com/blog/receive-mailgun-webhooks-locally/blog/how-to-test-webhooks/) and [What is a webhook](https://webhookrelay.com/blog/receive-mailgun-webhooks-locally/blog/what-is-webhook/). ## [Step 2: Forward the events to localhost with the relay agent](#step-2-forward-the-events-to-localhost-with-the-relay-agent) Once you know the payload, route those same events into your local handler. [Sign up for Webhook Relay](https://my.webhookrelay.com/register), install the `relay` agent (CLI or Docker), and create a bucket — say `mailgun`. The bucket gives you a stable public input endpoint. Start forwarding to your local server: ``` relay forward --bucket mailgun http://localhost:8080/webhook ``` The agent opens an **outbound** connection to Webhook Relay and streams every incoming request down to `http://localhost:8080/webhook`. Because the connection is outbound, there are **no firewall ports to open and no public IP needed** — this works from your laptop, behind a corporate proxy, or inside a Kubernetes cluster. Running in Docker? The same command works in the official `webhookrelay/webhookrelayd` image. Full details are in the [localhost forwarding docs](https://webhookrelay.com/blog/receive-mailgun-webhooks-locally/docs/webhooks/internal/localhost/). Now point your Mailgun webhook at the Webhook Relay endpoint (or just create it there from the start), send a test message, and watch the event arrive on `localhost`. ## [Mailgun-specific configuration and quirks](#mailgun-specific-configuration-and-quirks) A few Mailgun details worth knowing: - **Webhooks are per event type.** In **Sending → Webhooks** you register a URL against each event you care about: `delivered`, `opened`, `clicked`, `bounced` (permanent failures), `temporary_fail`, `complained`, `unsubscribed`, and more. You can point every event type at the same Webhook Relay endpoint and branch in your handler on the `event` field of `event-data`. - **Configure it in the dashboard or via the API.** The same webhooks can be created programmatically through the Mailgun webhooks API if you prefer infrastructure-as-code. - **Test from the dashboard.** Each webhook has a **Test webhook** button that sends a sample payload — handy for a first round trip. - **Retries.** If your endpoint does not return a 2xx, Mailgun retries on a back-off schedule, so a flaky local handler gets a second chance rather than silently dropping the event. - **Opened and clicked require tracking.** Those events only fire when open/click tracking is enabled. Need to deliver the same events to more than one place — say your local handler _and_ a Slack channel? That is a server-side fan-out, covered in [Mailgun webhook fanout](https://webhookrelay.com/blog/receive-mailgun-webhooks-locally/blog/mailgun-webhook-fanout/). You can also reshape the payload in flight with [webhook transformations](https://webhookrelay.com/blog/receive-mailgun-webhooks-locally/features/transform-webhooks/). ## [Step 3: Verify the Mailgun webhook signature](#step-3-verify-the-mailgun-webhook-signature) Mailgun signs every webhook so you can confirm it genuinely came from Mailgun. Inside the payload's `signature` object you get three values: `timestamp`, `token`, and `signature`. To verify, **concatenate the `timestamp` and the `token`** (in that order, with no separator) and compute an **HMAC-SHA256** over that string, keyed with your **Mailgun HTTP webhook signing key** (found in your dashboard's API security / signing key settings). Render the result as hex and compare it in constant time to the `signature` value. Note the important detail: the HMAC covers **`timestamp + token`**, _not_ the request body — a common point of confusion if you have implemented other providers that sign the raw body. As an extra guard, reject requests whose `timestamp` is too old and cache the one-time `token` to block replays. To sanity-check your implementation, paste the values and your signing key into the free [HMAC signature verifier](https://webhookrelay.com/blog/receive-mailgun-webhooks-locally/hmac-verification/). For language-specific code and the common pitfalls, read [Verify a webhook signature](https://webhookrelay.com/blog/receive-mailgun-webhooks-locally/blog/verify-webhook-signature/). ## [Replay and iterate](#replay-and-iterate) This is where local development gets fast: - **Replay from Mailgun** by re-sending a test webhook from the dashboard, or by triggering the event again. - **Replay from Webhook Relay** — past requests are stored on your bucket, so you can resend a captured `bounced` or `complained` event without touching Mailgun at all. - **Iterate on your handler** by editing code and replaying the same delivery until it behaves correctly. No commits, no pushes, no deploys just to test one code path. Because the Webhook Relay endpoint is stable, you can stop and restart the agent, reboot your machine, or come back next week — the Mailgun configuration never needs to change. ## [Get started](#get-started) 1. Inspect the real payload in the free [Webhook Bin](https://webhookrelay.com/blog/receive-mailgun-webhooks-locally/webhook-bin/) — no signup needed. 2. [Create a Webhook Relay account](https://my.webhookrelay.com/register), install the agent, and run `relay forward --bucket mailgun http://localhost:8080/webhook`. 3. Point your Mailgun webhook at the stable endpoint, send a test message, and watch the event hit `localhost`. You will be testing real Mailgun events against your local handler in a few minutes — no deploys, no open firewall ports, and a URL you configure exactly once. --- --- title: Receive Razorpay Webhooks Locally | WebhookRelay meta: "og: title": "Receive Razorpay Webhooks Locally" description: Test Razorpay webhooks on localhost without deploying. Inspect the real payment.captured payload and verify the X-Razorpay-Signature header. url: https://webhookrelay.com/blog/receive-razorpay-webhooks-locally.md file: /blog/receive-razorpay-webhooks-locally.md --- ![Stripes](https://webhookrelay.com/blog/receive-razorpay-webhooks-locally/images/stripes.svg) # **Receive Razorpay Webhooks Locally** Test Razorpay webhooks on localhost without deploying. Inspect the real payment.captured payload and verify the X-Razorpay-Signature header. ![Receive Razorpay Webhooks Locally (Test Razorpay Webhooks on localhost)](https://webhookrelay.com/blog/receive-razorpay-webhooks-locally/images/blog/heroes/localhost.jpg) You are integrating Razorpay — capturing payments, reconciling refunds, fulfilling orders — and you need to see your handler react to a real `payment.captured` or `order.paid` event. The problem is immediate: Razorpay will only POST to a **public URL**, and your handler is running on `localhost:3000`. Razorpay has no way to reach it. The usual workarounds are slow. Deploying to a staging server for every code change kills your iteration speed. Copying a sample payload from the docs into curl gives you a _guess_ at the real request, not the exact headers and raw body Razorpay actually sends — and getting the body byte-for-byte right matters a lot when you later verify the signature. What you want is to **test Razorpay webhooks locally** — real events, hitting your local handler, on a URL that does not change every time you restart. This guide shows how to do exactly that. ## [Why testing Razorpay webhooks on localhost is tricky](#why-testing-razorpay-webhooks-on-localhost-is-tricky) A webhook is just an HTTP request Razorpay sends to a URL when something happens — a payment is captured, an order is paid, a refund is processed. Razorpay sits on the public internet; your dev machine usually does not. It is behind a router, a corporate firewall, or both, with no public IP and no inbound ports open. So you need something in the middle: a public endpoint Razorpay can hit, that relays each request down to your laptop without you opening a single firewall port. That is what [Webhook Relay](https://my.webhookrelay.com/register) does — and unlike a random tunnel URL, the endpoint is **stable**, so you configure Razorpay once and never touch it again. ## [Step 1: Inspect the real payload with Webhook Bin](#step-1-inspect-the-real-payload-with-webhook-bin) Before you write any handler code, find out what Razorpay actually sends. Open the free [Webhook Bin](https://webhookrelay.com/blog/receive-razorpay-webhooks-locally/webhook-bin/) — no signup — and you get an instant public URL. 1. Copy the Webhook Bin URL. 2. In the Razorpay Dashboard, go to **Account & Settings → Webhooks** and click **Add New Webhook**. 3. Paste the URL into **Webhook URL**, leave the **Secret** blank for now (you will add one in Step 3), and select the **Active Events** you care about — for example `payment.captured`, `payment.failed`, and `order.paid`. 4. Save the webhook. Razorpay lets you fire a test event from the Dashboard, or you can trigger a real one in **Test Mode** (a test payment, a refund). Either way the request lands in Webhook Bin right away, and you can inspect the full JSON body, the query string, and every header — including `X-Razorpay-Signature`, `X-Razorpay-Event-Id`, and the `Content-Type: application/json`. Now you know the exact shape of the data before writing a line of code: the top-level `event` field (e.g. `payment.captured`), the `payload.payment.entity` object with its `id`, `amount`, `currency`, and `status`. For more on this approach, see [How to test webhooks](https://webhookrelay.com/blog/receive-razorpay-webhooks-locally/blog/how-to-test-webhooks/) and [What is a webhook](https://webhookrelay.com/blog/receive-razorpay-webhooks-locally/blog/what-is-webhook/). ## [Step 2: Forward the events to localhost with the relay agent](#step-2-forward-the-events-to-localhost-with-the-relay-agent) Once you know the payload, route those same events into your local handler. [Sign up for Webhook Relay](https://my.webhookrelay.com/register), install the `relay` agent (CLI or Docker), and create a bucket — say `razorpay`. The bucket gives you a stable public input endpoint. Start forwarding to your local server: ``` relay forward --bucket razorpay http://localhost:3000/webhooks/razorpay ``` The agent opens an **outbound** connection to Webhook Relay and streams every incoming request down to `http://localhost:3000/webhooks/razorpay`. Because the connection is outbound, there are **no firewall ports to open and no public IP needed** — this works from your laptop, behind a corporate proxy, or inside a Kubernetes cluster. Running in Docker? The same command works in the official `webhookrelay/webhookrelayd` image. Full details are in the [localhost forwarding docs](https://webhookrelay.com/blog/receive-razorpay-webhooks-locally/docs/webhooks/internal/localhost/). Now update the webhook's **Webhook URL** in the Razorpay Dashboard to your Webhook Relay endpoint (or just create it there from the start). Trigger a test payment and watch it arrive on `localhost`. ## [Razorpay-specific configuration and quirks](#razorpay-specific-configuration-and-quirks) A few Razorpay details worth knowing: - **Where to add it:** **Account & Settings → Webhooks** in the Dashboard. Test Mode and Live Mode each have their own webhooks — set one up in Test Mode for local development. - **Active Events:** pick exactly the events you handle (`payment.captured`, `payment.failed`, `payment.authorized`, `order.paid`, `refund.processed`, …). Branch on the top-level `event` field in your handler. - **Raw body matters:** Razorpay's signature is computed over the **raw** request body. If your framework parses JSON before you can read the bytes, capture the raw body first — re-serialized JSON will not match. - **Duplicate deliveries:** Razorpay may resend an event. Use the `X-Razorpay-Event-Id` header (unique per event) to make your handler idempotent. - **Order of events:** `payment.authorized` can arrive before `payment.captured` for the same payment, so design your handler to tolerate out-of-order and repeated events. ## [Step 3: Verify the Razorpay webhook signature](#step-3-verify-the-razorpay-webhook-signature) Set a **Secret** on the webhook and Razorpay will sign every request. It computes an **HMAC-SHA256** of the **raw** request body using your webhook secret and sends the **hex** digest in the **`X-Razorpay-Signature`** header. Your handler should recompute the HMAC over the raw body with the same secret and compare in constant time before trusting the payload — a request that does not match should be rejected outright. To sanity-check your implementation, paste a captured body, your secret, and the received signature into the free [HMAC signature verifier](https://webhookrelay.com/blog/receive-razorpay-webhooks-locally/hmac-verification/). For language-specific code and the common pitfalls (reading the body after a JSON parser has consumed it, timing-safe comparison), read [Verify a webhook signature](https://webhookrelay.com/blog/receive-razorpay-webhooks-locally/blog/verify-webhook-signature/). ## [Replay and iterate](#replay-and-iterate) This is where local development gets fast: - **Resend from Razorpay** — the Dashboard lets you fire test events at your webhook on demand. - **Replay from Webhook Relay** — past requests are stored on your bucket, so you can resend a captured `payment.captured` event without touching Razorpay at all. The raw body and `X-Razorpay-Signature` are preserved, so your signature check runs against the real thing. - **Iterate on your handler** by editing code and replaying the same delivery until it behaves correctly. No redeploys just to test a refund path. Because the Webhook Relay endpoint is stable, you can stop and restart the agent, reboot your machine, or come back next week — the Razorpay configuration never needs to change. If you are also wiring up another payment provider, the same flow works for [Stripe webhooks on localhost](https://webhookrelay.com/blog/receive-razorpay-webhooks-locally/blog/receiving-stripe-webhooks-localhost/). ## [Get started](#get-started) 1. Inspect the real payload in the free [Webhook Bin](https://webhookrelay.com/blog/receive-razorpay-webhooks-locally/webhook-bin/) — no signup needed. 2. [Create a Webhook Relay account](https://my.webhookrelay.com/register), install the agent, and run `relay forward --bucket razorpay http://localhost:3000/webhooks/razorpay`. 3. Point your Razorpay webhook at the stable endpoint, trigger a test payment, and watch it hit `localhost`. You will be testing real Razorpay events against your local handler in a few minutes — no deploys, no open firewall ports, and a URL you configure exactly once. --- --- title: Receive Mollie Webhooks Locally | WebhookRelay meta: "og: title": "Receive Mollie Webhooks Locally" description: Test Mollie webhooks on localhost without deploying. Mollie posts only the payment id, so inspect the request and fetch status from the API. url: https://webhookrelay.com/blog/receive-mollie-webhooks-locally.md file: /blog/receive-mollie-webhooks-locally.md --- ![Stripes](https://webhookrelay.com/blog/receive-mollie-webhooks-locally/images/stripes.svg) # **Receive Mollie Webhooks Locally** Test Mollie webhooks on localhost without deploying. Mollie posts only the payment id, so inspect the request and fetch status from the API. ![Receive Mollie Webhooks Locally (Test Mollie Webhooks on localhost)](https://webhookrelay.com/blog/receive-mollie-webhooks-locally/images/blog/heroes/localhost.jpg) You are integrating Mollie — taking payments across iDEAL, cards, and SEPA — and you need to see your handler react when a payment moves to `paid`, `failed`, or `expired`. The problem is immediate: Mollie will only POST to a **public URL**, and your handler is running on `localhost:3000`. Mollie has no way to reach it. The usual workarounds are slow. Deploying to a staging server for every code change kills your iteration speed. And Mollie has a twist that catches people off guard: the webhook does **not** carry the payment details at all. So copying a "sample payload" from somewhere is doubly misleading. What you want is to **test Mollie webhooks locally** — real notifications, hitting your local handler, on a URL that does not change every time you restart. This guide shows how to do exactly that. ## [How Mollie's webhook is different (and why it matters)](#how-mollies-webhook-is-different-and-why-it-matters) Most providers POST the full event to your URL. Mollie does not. Its classic webhook is a deliberately **thin notification**: when a payment's status changes, Mollie sends an HTTP POST whose body is a single form-encoded parameter — `id=tr_xxxxxxxx` — and nothing else. No JSON object, no status field, no signature header. That is by design. The contract is: _"object `tr_xxxxxxxx` changed — go ask the API what it looks like now."_ Your handler takes the `id`, calls the Mollie API with your own API key, reads the authoritative `status`, and only then fulfils the order. The security benefit is real: because the status comes from an authenticated API call rather than the POST body, a forged request to your webhook can never mark an order as paid. This is also why classic Mollie webhooks ship **no HMAC signature** — there is nothing in the body worth signing. Knowing this up front saves you from writing a handler that trusts a payload that does not exist. ## [Why testing Mollie webhooks on localhost is tricky](#why-testing-mollie-webhooks-on-localhost-is-tricky) Mollie sits on the public internet; your dev machine usually does not. It is behind a router, a corporate firewall, or both, with no public IP and no inbound ports open. So you need something in the middle: a public endpoint Mollie can hit, that relays each request down to your laptop without you opening a single firewall port. That is what [Webhook Relay](https://my.webhookrelay.com/register) does — and unlike a random tunnel URL, the endpoint is **stable**, so the `webhookUrl` you set on your payments keeps working across restarts. ## [Step 1: Inspect the real request with Webhook Bin](#step-1-inspect-the-real-request-with-webhook-bin) Before you write any handler code, confirm for yourself exactly what Mollie sends. Open the free [Webhook Bin](https://webhookrelay.com/blog/receive-mollie-webhooks-locally/webhook-bin/) — no signup — and you get an instant public URL. 1. Copy the Webhook Bin URL. 2. Create a test payment via the Mollie API and pass the Webhook Bin URL as the `webhookUrl` field. (You can also set a default webhook on a profile in the Mollie Dashboard.) 3. Complete or cancel the payment in Mollie's test checkout. The notification lands in Webhook Bin right away. Inspect it and you will see exactly the shape described above: a `Content-Type: application/x-www-form-urlencoded` body containing only `id=tr_...`, and standard HTTP headers with **no** signature header. Seeing the real, tiny request makes the "fetch from the API" flow click. For more on this approach, see [How to test webhooks](https://webhookrelay.com/blog/receive-mollie-webhooks-locally/blog/how-to-test-webhooks/) and [What is a webhook](https://webhookrelay.com/blog/receive-mollie-webhooks-locally/blog/what-is-webhook/). ## [Step 2: Forward the notifications to localhost with the relay agent](#step-2-forward-the-notifications-to-localhost-with-the-relay-agent) Now route those same notifications into your local handler. [Sign up for Webhook Relay](https://my.webhookrelay.com/register), install the `relay` agent (CLI or Docker), and create a bucket — say `mollie`. The bucket gives you a stable public input endpoint. Start forwarding to your local server: ``` relay forward --bucket mollie http://localhost:3000/webhooks/mollie ``` The agent opens an **outbound** connection to Webhook Relay and streams every incoming request down to `http://localhost:3000/webhooks/mollie`. Because the connection is outbound, there are **no firewall ports to open and no public IP needed** — this works from your laptop, behind a corporate proxy, or inside a Kubernetes cluster. Running in Docker? The same command works in the official `webhookrelay/webhookrelayd` image. Full details are in the [localhost forwarding docs](https://webhookrelay.com/blog/receive-mollie-webhooks-locally/docs/webhooks/internal/localhost/). Now set the Webhook Relay endpoint as your `webhookUrl` (or in your dashboard profile), create a test payment, and watch the `id=tr_...` notification arrive on `localhost`. ## [What your local handler should do](#what-your-local-handler-should-do) Because the POST only carries an id, your handler's job is a fixed three-step dance: 1. **Read the `id`** from the form-encoded body (for a payment it is `tr_...`; subscriptions and orders use other prefixes). 2. **Fetch from the Mollie API** — `GET https://api.mollie.com/v2/payments/{id}` with your API key — to read the authoritative current `status` (`open`, `paid`, `failed`, `canceled`, `expired`). 3. **Act on the real status** — fulfil, retry, or ignore — and always return `200 OK` quickly so Mollie does not retry unnecessarily. A few Mollie quirks worth knowing: - **Retries:** if your endpoint does not return a `2xx`, Mollie retries the notification on a schedule. Make your handler idempotent — the same `id` may arrive several times. - **No status in the body:** never branch on anything in the POST body except the `id`. The status only exists in the API response. - **Test vs live keys:** fetch the payment with the API key from the **same** mode (test/live) the payment was created in, or the lookup will 404. ## [Verifying a Mollie webhook](#verifying-a-mollie-webhook) Classic Mollie webhooks have **no signature** to check — the verification _is_ the API fetch. Looking up the `id` with your secret API key is what proves the notification is genuine, because only a real payment in your account will resolve. That said, the moment you start adding HMAC-signed webhooks from other providers (Stripe, GitHub, Razorpay), you will want a repeatable way to check signatures. The free [HMAC signature verifier](https://webhookrelay.com/blog/receive-mollie-webhooks-locally/hmac-verification/) and the guide on how to [verify a webhook signature](https://webhookrelay.com/blog/receive-mollie-webhooks-locally/blog/verify-webhook-signature/) cover that flow — handy when your stack mixes Mollie with a provider that _does_ sign its payloads. ## [Replay and iterate](#replay-and-iterate) This is where local development gets fast: - **Replay from Webhook Relay** — past notifications are stored on your bucket, so you can resend the same `id=tr_...` POST without creating a brand-new payment in Mollie. Your handler re-runs the full fetch-and-fulfil flow against a real payment id. - **Iterate on your handler** by editing code and replaying the same notification until the API fetch, status handling, and order logic all behave. No redeploys to test the `expired` path. Because the Webhook Relay endpoint is stable, you can stop and restart the agent, reboot your machine, or come back next week — the `webhookUrl` on your payments never needs to change. If you are also wiring up another payment provider, the same flow works for [Stripe webhooks on localhost](https://webhookrelay.com/blog/receive-mollie-webhooks-locally/blog/receiving-stripe-webhooks-localhost/). ## [Get started](#get-started) 1. Inspect the real notification in the free [Webhook Bin](https://webhookrelay.com/blog/receive-mollie-webhooks-locally/webhook-bin/) — no signup needed. 2. [Create a Webhook Relay account](https://my.webhookrelay.com/register), install the agent, and run `relay forward --bucket mollie http://localhost:3000/webhooks/mollie`. 3. Point your Mollie `webhookUrl` at the stable endpoint, create a test payment, and watch the `id` notification hit `localhost`. You will be testing real Mollie notifications against your local handler in a few minutes — no deploys, no open firewall ports, and a URL you configure exactly once. --- --- title: Receive Datadog Webhooks Locally | WebhookRelay meta: "og: title": "Receive Datadog Webhooks Locally" description: Test Datadog webhooks on localhost without deploying. Inspect the real alert payload and forward monitor notifications straight to your handler. url: https://webhookrelay.com/blog/receive-datadog-webhooks-locally.md file: /blog/receive-datadog-webhooks-locally.md --- ![Stripes](https://webhookrelay.com/blog/receive-datadog-webhooks-locally/images/stripes.svg) # **Receive Datadog Webhooks Locally** Test Datadog webhooks on localhost without deploying. Inspect the real alert payload and forward monitor notifications straight to your handler. ![Receive Datadog Webhooks Locally (Test Datadog Webhooks on localhost)](https://webhookrelay.com/blog/receive-datadog-webhooks-locally/images/blog/heroes/localhost.jpg) You are wiring up an integration that reacts to Datadog alerts — paging a custom on-call system, opening a ticket, kicking off a remediation script — and you need to see your handler react to a real monitor alert. The problem is immediate: Datadog will only POST to a **public URL**, and your handler is running on `localhost:8080`. Datadog has no way to reach it. The usual workarounds are painful. Deploying to staging for every code change is slow, and guessing at the JSON from the docs is doubly unreliable with Datadog, because **you define the payload yourself** — there is no canonical body to copy. What you want is to **receive Datadog webhooks locally** — real alert notifications hitting your local handler, with a URL that does not change every time you restart. ## [Why testing Datadog webhooks locally is tricky](#why-testing-datadog-webhooks-locally-is-tricky) A Datadog webhook is an outbound HTTP request that Datadog sends to a URL when a monitor or alert that references it triggers. Datadog sits on the public internet; your dev machine usually does not. It is behind a router, a corporate firewall, or both, with no public IP and no inbound ports open. So you need something in the middle: a public endpoint Datadog can hit, that relays each request down to your laptop without you opening a single firewall port. That is what [Webhook Relay](https://my.webhookrelay.com/register) does — and unlike a random tunnel URL, the endpoint is **stable**, so you configure Datadog once and never touch it again. ## [Step 1: Inspect the real payload with Webhook Bin](#step-1-inspect-the-real-payload-with-webhook-bin) Before you write any handler code, see exactly what Datadog sends with _your_ template. Open the free [Webhook Bin](https://webhookrelay.com/blog/receive-datadog-webhooks-locally/webhook-bin/) — no signup — and you get an instant public URL. 1. Copy the Webhook Bin URL. 2. In Datadog, go to **Integrations → Webhooks** (the Webhooks integration tile). 3. Click **New** / **Add Webhook**, give it a name (for example `bin`), and paste the Webhook Bin URL. 4. Define a **custom payload** — usually JSON with `$`-variables, for example: ``` { "title": "$EVENT_TITLE", "type": "$ALERT_TYPE", "host": "$HOSTNAME", "body": "$EVENT_MSG", "link": "$LINK" } ``` 1. Save the webhook, then reference it from a monitor by adding `@webhook-bin` to the monitor's notification message. Use the monitor's **Test Notifications** button (or let it trigger) to fire it. The captured request in Webhook Bin shows the full JSON body with the `$`-variables already substituted, plus the query string and every header. Because you control the template, this is the fastest way to confirm your variables resolve the way you expect _before_ you write a line of handler code. For more on this approach, see [How to test webhooks](https://webhookrelay.com/blog/receive-datadog-webhooks-locally/blog/how-to-test-webhooks/) and [What is a webhook](https://webhookrelay.com/blog/receive-datadog-webhooks-locally/blog/what-is-webhook/). ## [Step 2: Forward the events to localhost with the relay agent](#step-2-forward-the-events-to-localhost-with-the-relay-agent) Once your payload looks right, route those same notifications into your local handler. [Sign up for Webhook Relay](https://my.webhookrelay.com/register), install the `relay` agent (CLI or Docker), and create a bucket — say `datadog`. The bucket gives you a stable public input endpoint. Start forwarding to your local server: ``` relay forward --bucket datadog http://localhost:8080/webhook ``` The agent opens an **outbound** connection to Webhook Relay and streams every incoming request down to `http://localhost:8080/webhook`. Because the connection is outbound, there are **no firewall ports to open and no public IP needed** — this works from your laptop, behind a corporate proxy, or inside a Kubernetes cluster. Running in Docker? The same command works in the official `webhookrelay/webhookrelayd` image. Full details are in the [localhost forwarding docs](https://webhookrelay.com/blog/receive-datadog-webhooks-locally/docs/webhooks/internal/localhost/). Now set the webhook's **URL** in the Datadog Webhooks integration to your Webhook Relay endpoint (or create it there from the start), trigger the monitor, and watch the alert arrive on `localhost`. ## [Datadog-specific configuration and quirks](#datadog-specific-configuration-and-quirks) A few Datadog details worth knowing: - **It is a notification handle, not a per-event subscription.** You define the webhook once in **Integrations → Webhooks**, then opt monitors in by putting `@webhook-` in their notification text. Different monitors can target the same webhook. - **The payload is yours to design.** Leave the custom payload empty to get Datadog's default JSON, or define your own template with built-in `$`-variables (`$EVENT_TITLE`, `$EVENT_MSG`, `$ALERT_TYPE`, `$HOSTNAME`, `$ORG_NAME`, `$LINK`, `$SNAPSHOT`, and more) plus any **custom variables** you create in the tile. - **Custom headers and URL variables.** You can add custom request headers and even use `$`-variables inside the URL — useful for routing or auth tokens. - **Test Notifications.** When editing a monitor, **Test Notifications** sends a sample alert through the webhook so you can iterate without waiting for a real threshold breach. - **No provider signature.** Datadog does not HMAC-sign the body, so secure the endpoint another way — see verification below. If your local handler expects a different shape than the template produces — or you want to convert the alert into a Slack or Teams message — you can reshape it in flight with [webhook transformations](https://webhookrelay.com/blog/receive-datadog-webhooks-locally/features/transform-webhooks/) instead of editing the Datadog template each time. ## [Step 3: Verify and secure the request](#step-3-verify-and-secure-the-request) Datadog does not sign its webhooks with an HMAC the way GitHub or Stripe do, so verification is something you build into the payload and headers. Two common approaches: add a **custom header** (or a `$`-variable secret) in the Datadog webhook config and reject any request missing it, or include an **unguessable path segment** in the webhook URL so only Datadog knows where to POST. If you do introduce an HMAC-style shared secret of your own, validate it the same way you would any signed webhook — paste the body, secret, and computed digest into the free [HMAC signature verifier](https://webhookrelay.com/blog/receive-datadog-webhooks-locally/hmac-verification/) to confirm your logic. For the general pattern and language-specific code, read [Verify a webhook signature](https://webhookrelay.com/blog/receive-datadog-webhooks-locally/blog/verify-webhook-signature/). ## [Replay and iterate](#replay-and-iterate) This is where local development gets fast: - **Replay from Datadog** by hitting **Test Notifications** again, or re-triggering the monitor. - **Replay from Webhook Relay** — past requests are stored on your bucket, so you can resend a captured alert without touching Datadog at all. - **Iterate on your handler** by editing code and replaying the same delivery until it behaves correctly. No deploys just to test one alert path. Because the Webhook Relay endpoint is stable, you can stop and restart the agent, reboot your machine, or come back next week — the Datadog configuration never needs to change. ## [Get started](#get-started) 1. Inspect the real payload in the free [Webhook Bin](https://webhookrelay.com/blog/receive-datadog-webhooks-locally/webhook-bin/) — no signup needed. 2. [Create a Webhook Relay account](https://my.webhookrelay.com/register), install the agent, and run `relay forward --bucket datadog http://localhost:8080/webhook`. 3. Point your Datadog webhook at the stable endpoint, add `@webhook-` to a monitor, trigger it, and watch the alert hit `localhost`. You will be testing real Datadog alerts against your local handler in a few minutes — no deploys, no open firewall ports, and a URL you configure exactly once. --- --- title: Test WhatsApp Webhooks Locally | WebhookRelay meta: "og: title": "Test WhatsApp Webhooks Locally" description: Test WhatsApp Cloud API webhooks locally on localhost. Pass Meta's verify-token GET handshake, inspect message payloads, and verify the X-Hub-Signature-256. url: https://webhookrelay.com/blog/receive-whatsapp-webhooks-locally.md file: /blog/receive-whatsapp-webhooks-locally.md --- ![Stripes](https://webhookrelay.com/blog/receive-whatsapp-webhooks-locally/images/stripes.svg) # **Test WhatsApp Webhooks Locally** Test WhatsApp Cloud API webhooks locally on localhost. Pass Meta's verify-token GET handshake, inspect message payloads, and verify the X-Hub-Signature-256. ![Test WhatsApp Webhooks Locally (WhatsApp Cloud API on localhost)](https://webhookrelay.com/blog/receive-whatsapp-webhooks-locally/images/blog/heroes/localhost.jpg) You are building a WhatsApp integration on the **Meta WhatsApp Cloud API** — a chatbot, a notification responder, a support inbox — and you need to watch your handler react to a real inbound `messages` or delivery `statuses` event. The problem is immediate: Meta will only call a **public URL** over HTTPS, and your handler is running on `localhost:8080`. Meta has no way to reach it. The usual workarounds are painful. Deploying to a staging server for every code change is slow. And WhatsApp adds a twist most providers do not: before any events flow, Meta runs a **verification handshake** — a GET request your endpoint has to answer correctly, or the webhook will not save at all. Copying a sample payload into curl never exercises that handshake. What you want is to **test WhatsApp webhooks locally** — real verification, real messages, hitting your local handler, on a URL that does not change every time you restart. This guide shows how to do exactly that. ## [Why testing WhatsApp Cloud API webhooks locally is tricky](#why-testing-whatsapp-cloud-api-webhooks-locally-is-tricky) A webhook is just an HTTP request Meta sends to a URL when something happens — a customer messages your number, or a message you sent changes status. Meta lives on the public internet; your dev machine usually does not. It sits behind a router, a corporate firewall, or both, with no public IP and no inbound ports open. Meta also requires a valid HTTPS endpoint — self-signed certificates are rejected. So you need something in the middle: a public HTTPS endpoint Meta can hit that relays each request down to your laptop without you opening a single firewall port or wrangling a TLS certificate. That is what [Webhook Relay](https://my.webhookrelay.com/register) does — and unlike a random tunnel URL, the endpoint is **stable**, so you pass Meta's verification once and never touch it again. ## [Step 1: Forward to localhost with the relay agent](#step-1-forward-to-localhost-with-the-relay-agent) Because WhatsApp's setup begins with a verification call to _your_ endpoint, get the tunnel to localhost running first. [Sign up for Webhook Relay](https://my.webhookrelay.com/register), install the `relay` agent (CLI or Docker), and create a bucket — say `whatsapp`. The bucket gives you a stable public HTTPS input endpoint. Start forwarding to your local server: ``` relay forward --bucket whatsapp http://localhost:8080/webhook ``` The agent opens an **outbound** connection to Webhook Relay and streams every incoming request down to `http://localhost:8080/webhook`. Because the connection is outbound, there are **no firewall ports to open and no public IP needed** — this works from your laptop, behind a corporate proxy, or inside a Kubernetes cluster. Running in Docker? The same command works in the official `webhookrelay/webhookrelayd` image. Full details are in the [localhost forwarding docs](https://webhookrelay.com/blog/receive-whatsapp-webhooks-locally/docs/webhooks/internal/localhost/). ## [Step 2: Pass Meta's verify-token GET handshake](#step-2-pass-metas-verify-token-get-handshake) This is the step that trips people up, so be precise. When you register the Callback URL in the Meta App Dashboard, Meta sends a **GET** request to your endpoint with three query parameters: - `hub.mode` — always `subscribe` - `hub.verify_token` — the exact string you typed into the dashboard's **Verify token** field - `hub.challenge` — a random string Meta wants echoed back Your handler must check that `hub.mode` is `subscribe` **and** that `hub.verify_token` matches your token, then respond with HTTP `200` and the value of `hub.challenge` as the **plain response body** (just the challenge string, nothing else). If the token does not match, respond `403`. Until Meta receives the correct challenge echo, it will not save the webhook. A minimal handler looks like this: ``` @app.get("/webhook") def verify(): if (request.args.get("hub.mode") == "subscribe" and request.args.get("hub.verify_token") == VERIFY_TOKEN): return request.args.get("hub.challenge"), 200 return "Forbidden", 403 ``` Now wire it up: in the [Meta App Dashboard](https://my.webhookrelay.com/register) go to **WhatsApp → Configuration → Webhook**, click **Edit**, paste your Webhook Relay endpoint as the **Callback URL**, enter your **Verify token**, and click **Verify and save**. With the relay agent running and your GET handler returning the challenge, the handshake passes and the webhook is saved. ## [Step 3: Subscribe and inspect the real payload](#step-3-subscribe-and-inspect-the-real-payload) Once the URL is verified, choose which events you want. Under the same **Configuration → Webhook** panel, click **Manage** and subscribe to the fields you need — typically **messages** (inbound messages and **statuses** updates arrive under this field). Want to see the exact event shape before coding against it? Point the Callback URL at a free [Webhook Bin](https://webhookrelay.com/blog/receive-whatsapp-webhooks-locally/webhook-bin/) instead — but note the catch: Webhook Bin returns a generic `200`, not the `hub.challenge`, so it is great for **inspecting** the verification GET and the message POSTs, but Meta will not finalize the subscription until your real handler echoes the challenge. Use the Bin to learn the payload, then switch to your bucket. Send a WhatsApp message to your test number and inspect the captured POST: a JSON body with the `object` set to `whatsapp_business_account`, an `entry` array, and nested `changes` containing the `messages` or `statuses` payload. You will also see the headers, including `X-Hub-Signature-256`. For more on this approach, see [How to test webhooks](https://webhookrelay.com/blog/receive-whatsapp-webhooks-locally/blog/how-to-test-webhooks/) and [What is a webhook](https://webhookrelay.com/blog/receive-whatsapp-webhooks-locally/blog/what-is-webhook/). ## [Step 4: Verify the X-Hub-Signature-256 signature](#step-4-verify-the-x-hub-signature-256-signature) Every event POST is signed. Meta computes an **HMAC-SHA256** of the **raw** request body using your Meta app's **App Secret** and sends the digest in the **`X-Hub-Signature-256`** header, formatted as `sha256=...`. Your handler should recompute the HMAC over the raw body and compare it to the header in constant time before trusting the message. The classic pitfall: if your framework parses the JSON first and you re-serialize it, the bytes change and the signature will never match. Capture the **raw** body for the comparison. To sanity-check your implementation, paste a captured body, your App Secret, and the received signature into the free [HMAC signature verifier](https://webhookrelay.com/blog/receive-whatsapp-webhooks-locally/hmac-verification/). For language-specific code and more edge cases, read [Verify a webhook signature](https://webhookrelay.com/blog/receive-whatsapp-webhooks-locally/blog/verify-webhook-signature/). ## [Replay and iterate](#replay-and-iterate) This is where local development gets fast: - **Replay from Webhook Relay** — past requests are stored on your bucket, so you can resend a captured message event against your handler without sending a new WhatsApp message every time. - **Iterate on your handler** by editing code and replaying the same delivery until it behaves correctly. No deploys just to test a single branch. - **Keep `relay forward` running** while you work — both the verification GET and live message POSTs stream straight to `localhost`. Because the Webhook Relay endpoint is stable, you can stop and restart the agent, reboot your machine, or come back next week — the Meta configuration and the verify token never need to change. ## [Get started](#get-started) 1. [Create a Webhook Relay account](https://my.webhookrelay.com/register), install the agent, and run `relay forward --bucket whatsapp http://localhost:8080/webhook`. 2. In **Meta App Dashboard → WhatsApp → Configuration**, set the Callback URL to your stable endpoint, enter your verify token, and let your local GET handler echo the `hub.challenge`. 3. Subscribe to **messages**, send a test message, and watch the signed event hit `localhost`. You will be testing real WhatsApp Cloud API events against your local handler in a few minutes — verify-token handshake included, no deploys, no open firewall ports, and a URL you configure exactly once. --- --- title: How to receive Paypal webhooks on localhost | WebhookRelay meta: "og: title": "How to receive Paypal webhooks on localhost" description: Often when building an application that integrates with 3rd party services we need a way to receive webhooks url: https://webhookrelay.com/blog/receiving-paypal-webhooks-localhost.md file: /blog/receiving-paypal-webhooks-localhost.md --- ![Stripes](https://webhookrelay.com/blog/receiving-paypal-webhooks-localhost/images/stripes.svg) # **How to receive Paypal webhooks on localhost** Often when building an application that integrates with 3rd party services we need a way to receive webhooks ![PayPal webhooks](https://webhookrelay.com/blog/receiving-paypal-webhooks-localhost/images/blog/paypal-webhooks/paypal-webhooks.png) In this article, we will write a small web server in my favorite language Go that handles PayPal's webhooks. I will show you how easy it is to start receiving webhooks, developing your application and debugging it with Webhook Relay. According to PayPal webhook [docs](https://developer.paypal.com/docs/integration/direct/webhooks/rest-webhooks/#): > Webhooks are HTTP callbacks that receive notification messages for events. To create a webhook at PayPal, users configure a webhook listener and subscribe it to events. A webhook listener is a server that listens at a specific URL for incoming HTTP POST notification messages that are triggered when events occur. PayPal signs each notification message that it delivers to your webhook listener. So, nothing unusual or new. Webhooks are important - they are used in pretty much any SaaS application that has a subscription model. With webhooks you can: - Enable/disable features when a customer pays for the plan. - Email a plan change confirmation to your customer. ## [Setting up the environment](#setting-up-the-environment) - Download CLI and register to get your key & secret. Instructions can be found [here](https://docs.webhookrelay.com/installation-options/installation-options/install-cli). If you have done that already (I guess most of the planet have already registered), you are good to go to the next step! - Download and install Go, instructions [here](https://golang.org/doc/install). Unlike other languages that install an astronomical number of packages, using Go you can usually get around with just a handful of helper packages. - PayPal (probably no surprise here) - we will be using [developer dashboard](https://developer.paypal.com). First things first, create a webhook. Go to the [PayPal Dashboard](https://developer.paypal.com/developer/applications/) and create an app, then select the app in which you want to enable webhooks: ![create PayPal app](https://webhookrelay.com/blog/receiving-paypal-webhooks-localhost/images/blog/paypal-webhooks/create-app.png) Now, let's get our public Webhook Relay endpoint. Assuming that our future app will accept webhooks on _[http://localhost:8080/v1/paypal-webhooks](http://localhost:8080/v1/paypal-webhooks)_, start forwarding them there: ``` $ relay forward -b paypal http://localhost:8080/v1/paypal-webhooks Forwarding: https://my.webhookrelay.com/v1/webhooks/672d0c8f-b742-4a99-96c1-af4de65bc02a -> http://localhost:8080/v1/paypal-webhooks Starting webhook relay agent... 1.5345938246082163e+09 info webhook relay ready... {"host": "my.webhookrelay.com:8080"} ``` Here we can see `https://my.webhookrelay.com/v1/webhooks/672d0c8f-b742-4a99-96c1-af4de65bc02a` (you will have a different ID:), this is our public endpoint which we can supply to PayPal: ![paypal webhook endpoint](https://webhookrelay.com/blog/receiving-paypal-webhooks-localhost/images/blog/paypal-webhooks/webhook-endpoint.png) Let's tick "All events" as we really don't care about it now, we can always un-tick boxes. Scroll to the bottom and click "Save". ## [Implementing webhook handling](#implementing-webhook-handling) Time to write some Go. I have started this article thinking it's going to be about PayPal's IPNs but turns out it's about webhooks, I will write another one about IPNs later one. We will create a small application that will be handling our webhooks: ``` // main.go package main import ( "encoding/json" "fmt" "io/ioutil" "log" "net/http" "time" ) const ( debug = false ) func main() { mux := http.NewServeMux() listener := New(debug) mux.Handle("/v1/paypal-webhooks", listener.WebhooksHandler(func(err error, n *PaypalNotification) { if err != nil { log.Printf("IPN error: %v", err) return } log.Printf("event type: %s", n.EventType) log.Printf("event resource type: %s", n.ResourceType) log.Printf("summary: %s", n.Summary) })) log.Println("server starting on :8080") log.Fatalf("failed to run http server: %v", http.ListenAndServe(":8080", mux)) } type Listener struct { debug bool } func New(debug bool) *Listener { return &Listener{ debug: debug, } } // Listen for webhooks func (l *Listener) WebhooksHandler(cb func(err error, n *PaypalNotification)) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { body, err := ioutil.ReadAll(r.Body) if err != nil { cb(fmt.Errorf("failed to read body: %s", err), nil) return } var notification PaypalNotification err = json.Unmarshal(body, ¬ification) if err != nil { cb(fmt.Errorf("failed to decode request body: %s", err), nil) return } if l.debug { fmt.Printf("paypal: body: %s, parsed: %+v\n", body, notification) } w.WriteHeader(http.StatusOK) cb(nil, ¬ification) } } type PaypalNotification struct { ID string \`json:"id"\` CreateTime time.Time \`json:"create_time"\` ResourceType string \`json:"resource_type"\` EventType string \`json:"event_type"\` Summary string \`json:"summary"\` Resource struct { ParentPayment string \`json:"parent_payment"\` UpdateTime time.Time \`json:"update_time"\` Amount struct { Total string \`json:"total"\` Currency string \`json:"currency"\` } \`json:"amount"\` CreateTime time.Time \`json:"create_time"\` Links []struct { Href string \`json:"href"\` Rel string \`json:"rel"\` Method string \`json:"method"\` } \`json:"links"\` ID string \`json:"id"\` State string \`json:"state"\` } \`json:"resource"\` Links []struct { Href string \`json:"href"\` Rel string \`json:"rel"\` Method string \`json:"method"\` EncType string \`json:"encType"\` } \`json:"links"\` EventVersion string \`json:"event_version"\` } ``` Code can be found here: [https://github.com/webhookrelay/paypal-ipn](https://github.com/webhookrelay/paypal-ipn). To start our app, we simply: ``` $ go run main.go 2018/08/18 20:01:53 server starting on :8080 ``` In real life, you would probably want to use `go install` (makes builds a lot faster). ## [Enter the webhook simulator](#enter-the-webhook-simulator) Go to **Webhooks Simulator** under **MOCK** section. Now enter again our public Webhook Relay endpoint and let's select "Payment capture completed": ![webhooks simulator](https://webhookrelay.com/blog/receiving-paypal-webhooks-localhost/images/blog/paypal-webhooks/webhooks-simulator.png) Click "Send Test". In a few seconds we should see in our app received webhook: ``` $ go run example/main.go 2018/08/18 20:01:53 server starting on :8080 2018/08/18 20:02:10 event type: PAYMENT.CAPTURE.COMPLETED 2018/08/18 20:02:10 event resource type: capture 2018/08/18 20:02:10 summary: Payment completed for $ 7.47 USD ``` Congrats, we just earned 7.47 imaginary USD! P.S. Not sure why, but it sometimes takes more time for them to send those dummy webhook so if nothing happens after you click that button, just wait a bit. Or once you have the webhook already in Webhook Relay, you can retry from there, they get forwarded instantly. ## [Debugging webhooks](#debugging-webhooks) We can also see all webhooks that are sent through the Wehboook Relay in our buckets page: ![paypal webhook](https://webhookrelay.com/blog/receiving-paypal-webhooks-localhost/images/blog/paypal-webhooks/paypal-bucket.png) You can inspect and resend webhooks from here. It's also a good place to get the initial JSON structure and convert it to Go struct using this nice website: [https://mholt.github.io/json-to-go/](https://mholt.github.io/json-to-go/). ## [Wrapping up](#wrapping-up) Webhook Relay makes it really straightforward to receive webhooks on localhost or private networks. It can be used not only in development but in production as well if you don't want or can't expose your webhooks processing server to the internet. If you are working with Stripe, check out my previous blog post [here](https://webhookrelay.com/blog/receiving-paypal-webhooks-localhost/blog/receiving-stripe-webhooks-localhost/). --- --- title: Test WorkOS Webhooks Locally | WebhookRelay meta: "og: title": "Test WorkOS Webhooks Locally" description: Test WorkOS webhooks locally and receive them on localhost without deploying. Inspect the real payload, forward to your handler, and verify the signature. url: https://webhookrelay.com/blog/receive-workos-webhooks-locally.md file: /blog/receive-workos-webhooks-locally.md --- ![Stripes](https://webhookrelay.com/blog/receive-workos-webhooks-locally/images/stripes.svg) # **Test WorkOS Webhooks Locally** Test WorkOS webhooks locally and receive them on localhost without deploying. Inspect the real payload, forward to your handler, and verify the signature. ![Test WorkOS Webhooks Locally (Receive WorkOS Webhooks on localhost)](https://webhookrelay.com/blog/receive-workos-webhooks-locally/images/blog/heroes/localhost.jpg) You are building a WorkOS integration and you need to watch your handler react to a real event. The problem hits immediately: WorkOS will only POST to a **public URL**, and your handler is running on `localhost:8080`. WorkOS has no way to reach it. The usual workarounds are slow. Deploying to a staging environment for every code change kills your iteration speed. Copying a sample payload out of the docs into curl gives you a _guess_ at the real request, not the exact headers and body WorkOS actually sends. What you really want is to **test WorkOS webhooks locally** — real events, hitting your local handler, on a URL that does not change every time you restart. This guide shows how to do exactly that. ## [Why testing WorkOS webhooks locally is tricky](#why-testing-workos-webhooks-locally-is-tricky) A webhook is just an HTTP request that WorkOS sends to a URL when something changes. WorkOS lives on the public internet; your dev machine usually does not. It sits behind a router, a corporate firewall, or both, with no public IP and no inbound ports open. So you need something in the middle: a public endpoint WorkOS can hit that relays each request down to your laptop without you opening a single firewall port. That is what [Webhook Relay](https://my.webhookrelay.com/register) does — and unlike a random tunnel URL, the endpoint is **stable**, so you configure WorkOS once and never touch it again. ## [Step 1: Inspect the real payload with Webhook Bin](#step-1-inspect-the-real-payload-with-webhook-bin) Before you write any handler code, find out what WorkOS actually sends. Open the free [Webhook Bin](https://webhookrelay.com/blog/receive-workos-webhooks-locally/webhook-bin/) — no signup — and you get an instant public URL. 1. Copy the Webhook Bin URL. 2. In the WorkOS Dashboard, go to **Webhooks**, click **Create Endpoint**, paste the URL, and select the events you want. 3. Trigger a real event and inspect the captured request. You will see the full body and every header. The body is JSON with top-level `event`, `id`, `data` and `created_at` keys. The `data` object mirrors the resource that changed (a directory user, a group membership, an SSO connection). Now you know the exact shape of the data before writing a line of code. For more on this approach, see [How to test webhooks](https://webhookrelay.com/blog/receive-workos-webhooks-locally/blog/how-to-test-webhooks/) and [What is a webhook](https://webhookrelay.com/blog/receive-workos-webhooks-locally/blog/what-is-webhook/). ## [Step 2: Forward the events to localhost with the relay agent](#step-2-forward-the-events-to-localhost-with-the-relay-agent) Once you know the payload, route those same events into your local handler. [Sign up for Webhook Relay](https://my.webhookrelay.com/register), install the `relay` agent (CLI or Docker), and create a bucket — say `workos`. The bucket gives you a stable public input endpoint. Start forwarding to your local server: ``` relay forward --bucket workos http://localhost:8080/webhook ``` The agent opens an **outbound** connection to Webhook Relay and streams every incoming request down to `http://localhost:8080/webhook`. Because the connection is outbound, there are **no firewall ports to open and no public IP needed** — this works from your laptop, behind a corporate proxy, or inside a Kubernetes cluster. Running in Docker? The same command works in the official `webhookrelay/webhookrelayd` image. Full details are in the [localhost forwarding docs](https://webhookrelay.com/blog/receive-workos-webhooks-locally/docs/webhooks/internal/localhost/). Now point the WorkOS webhook at your Webhook Relay endpoint (or create it there from the start), trigger an event, and watch it arrive on `localhost`. ## [WorkOS-specific configuration and quirks](#workos-specific-configuration-and-quirks) A few WorkOS details worth knowing: - **Where to add it:** the WorkOS Dashboard → **Webhooks** → **Create Endpoint**. Each endpoint has its own signing secret. - **Events:** Directory Sync (`dsync.*`), SSO connection (`connection.*`) and other lifecycle events — subscribe only to what you need. - **Payload shape:** JSON with `event`, `id`, `data`, `created_at`. Branch on `event` in your handler. - **Respond fast:** acknowledge with a quick `2xx`; WorkOS retries failed deliveries with backoff. ## [Step 3: Verify the WorkOS webhook signature](#step-3-verify-the-workos-webhook-signature) WorkOS signs every request. It computes an **HMAC-SHA256** of `"{timestamp}.{raw body}"` using your endpoint's signing secret and sends it in the **`WorkOS-Signature`** header as `t=, v1=`. Your handler should split out the timestamp and signature, recompute the HMAC over `timestamp + "." + rawBody`, and compare in constant time. Reject requests whose timestamp is not recent to stop replays. To sanity-check an HMAC implementation, paste a captured body, your secret, and the received signature into the free [HMAC signature verifier](https://webhookrelay.com/blog/receive-workos-webhooks-locally/hmac-verification/). For language-specific code and the common pitfalls (reading the body _after_ a JSON parser has already consumed it, timing-safe comparison), read [Verify a webhook signature](https://webhookrelay.com/blog/receive-workos-webhooks-locally/blog/verify-webhook-signature/). ## [Replay and iterate](#replay-and-iterate) This is where local development gets fast: - **Replay from Webhook Relay** — past requests are stored on your bucket, so you can resend a captured event against your handler without touching WorkOS at all. - **Iterate on your handler** by editing code and replaying the same delivery until it behaves correctly. No commits, no pushes, no deploys just to test a single code path. - **Keep `relay forward` running** while you work — events stream straight to `localhost` as you trigger them in WorkOS. Because the Webhook Relay endpoint is stable, you can stop and restart the agent, reboot your machine, or come back next week — the WorkOS configuration never needs to change. ## [Get started](#get-started) 1. Inspect the real payload in the free [Webhook Bin](https://webhookrelay.com/blog/receive-workos-webhooks-locally/webhook-bin/) — no signup needed. 2. [Create a Webhook Relay account](https://my.webhookrelay.com/register), install the agent, and run `relay forward --bucket workos http://localhost:8080/webhook`. 3. Point your WorkOS webhook at the stable endpoint, trigger an event, and watch it hit `localhost`. You will be testing real WorkOS events against your local handler in a few minutes — no deploys, no open firewall ports, and a URL you configure exactly once. --- --- title: Test Plaid Webhooks Locally | WebhookRelay meta: "og: title": "Test Plaid Webhooks Locally" description: Test Plaid webhooks locally and receive them on localhost without deploying. Inspect the real payload, forward to your handler, and verify the signature. url: https://webhookrelay.com/blog/receive-plaid-webhooks-locally.md file: /blog/receive-plaid-webhooks-locally.md --- ![Stripes](https://webhookrelay.com/blog/receive-plaid-webhooks-locally/images/stripes.svg) # **Test Plaid Webhooks Locally** Test Plaid webhooks locally and receive them on localhost without deploying. Inspect the real payload, forward to your handler, and verify the signature. ![Test Plaid Webhooks Locally (Receive Plaid Webhooks on localhost)](https://webhookrelay.com/blog/receive-plaid-webhooks-locally/images/blog/heroes/localhost.jpg) You are building a Plaid integration and you need to watch your handler react to a real event. The problem hits immediately: Plaid will only POST to a **public URL**, and your handler is running on `localhost:8080`. Plaid has no way to reach it. The usual workarounds are slow. Deploying to a staging environment for every code change kills your iteration speed. Copying a sample payload out of the docs into curl gives you a _guess_ at the real request, not the exact headers and body Plaid actually sends. What you really want is to **test Plaid webhooks locally** — real events, hitting your local handler, on a URL that does not change every time you restart. This guide shows how to do exactly that. ## [Why testing Plaid webhooks locally is tricky](#why-testing-plaid-webhooks-locally-is-tricky) A webhook is just an HTTP request that Plaid sends to a URL when something changes. Plaid lives on the public internet; your dev machine usually does not. It sits behind a router, a corporate firewall, or both, with no public IP and no inbound ports open. So you need something in the middle: a public endpoint Plaid can hit that relays each request down to your laptop without you opening a single firewall port. That is what [Webhook Relay](https://my.webhookrelay.com/register) does — and unlike a random tunnel URL, the endpoint is **stable**, so you configure Plaid once and never touch it again. ## [Step 1: Inspect the real payload with Webhook Bin](#step-1-inspect-the-real-payload-with-webhook-bin) Before you write any handler code, find out what Plaid actually sends. Open the free [Webhook Bin](https://webhookrelay.com/blog/receive-plaid-webhooks-locally/webhook-bin/) — no signup — and you get an instant public URL. 1. Copy the Webhook Bin URL. 2. Set the webhook URL in the Plaid Dashboard under **Team Settings → Webhooks**, or pass `webhook` when you create an Item via the API. 3. Trigger a real event and inspect the captured request. You will see the full body and every header. The body is JSON keyed by `webhook_type` (e.g. `TRANSACTIONS`) and `webhook_code` (e.g. `SYNC_UPDATES_AVAILABLE`), plus an `item_id` and product-specific fields. You react to the type/code, then call the Plaid API to fetch the actual data. Now you know the exact shape of the data before writing a line of code. For more on this approach, see [How to test webhooks](https://webhookrelay.com/blog/receive-plaid-webhooks-locally/blog/how-to-test-webhooks/) and [What is a webhook](https://webhookrelay.com/blog/receive-plaid-webhooks-locally/blog/what-is-webhook/). ## [Step 2: Forward the events to localhost with the relay agent](#step-2-forward-the-events-to-localhost-with-the-relay-agent) Once you know the payload, route those same events into your local handler. [Sign up for Webhook Relay](https://my.webhookrelay.com/register), install the `relay` agent (CLI or Docker), and create a bucket — say `plaid`. The bucket gives you a stable public input endpoint. Start forwarding to your local server: ``` relay forward --bucket plaid http://localhost:8080/webhook ``` The agent opens an **outbound** connection to Webhook Relay and streams every incoming request down to `http://localhost:8080/webhook`. Because the connection is outbound, there are **no firewall ports to open and no public IP needed** — this works from your laptop, behind a corporate proxy, or inside a Kubernetes cluster. Running in Docker? The same command works in the official `webhookrelay/webhookrelayd` image. Full details are in the [localhost forwarding docs](https://webhookrelay.com/blog/receive-plaid-webhooks-locally/docs/webhooks/internal/localhost/). Now point the Plaid webhook at your Webhook Relay endpoint (or create it there from the start), trigger an event, and watch it arrive on `localhost`. ## [Plaid-specific configuration and quirks](#plaid-specific-configuration-and-quirks) A few Plaid details worth knowing: - **Where to set it:** the Plaid Dashboard (**Team Settings → Webhooks**) for a default URL, or per-Item by passing `webhook` at Item creation. - **Events:** webhooks are grouped by `webhook_type` and `webhook_code` — e.g. `TRANSACTIONS` / `SYNC_UPDATES_AVAILABLE`. The webhook is a _notification_; you then call the API to pull the data. - **Sandbox:** use the Plaid Sandbox and `/sandbox/item/fire_webhook` to trigger test webhooks into your bin while you build. - **Verification:** JWT/ES256, not HMAC — see the verification note below. ## [Step 3: Verify the Plaid webhook signature](#step-3-verify-the-plaid-webhook-signature) Plaid's verification is unusual: rather than a shared-secret HMAC, it sends a **JWT (JWS signed with ES256)** in the **`Plaid-Verification`** header. To verify, read the JWT header's `kid`, fetch the matching public key from Plaid's `/webhook_verification_key/get` endpoint (cache it; keys rotate), validate the JWT signature, check the `iat` is recent, and confirm a SHA-256 of the raw request body equals the JWT's `request_body_sha256` claim. Only then trust the payload. To sanity-check an HMAC implementation, paste a captured body, your secret, and the received signature into the free [HMAC signature verifier](https://webhookrelay.com/blog/receive-plaid-webhooks-locally/hmac-verification/). For language-specific code and the common pitfalls (reading the body _after_ a JSON parser has already consumed it, timing-safe comparison), read [Verify a webhook signature](https://webhookrelay.com/blog/receive-plaid-webhooks-locally/blog/verify-webhook-signature/). ## [Replay and iterate](#replay-and-iterate) This is where local development gets fast: - **Replay from Webhook Relay** — past requests are stored on your bucket, so you can resend a captured event against your handler without touching Plaid at all. - **Iterate on your handler** by editing code and replaying the same delivery until it behaves correctly. No commits, no pushes, no deploys just to test a single code path. - **Keep `relay forward` running** while you work — events stream straight to `localhost` as you trigger them in Plaid. Because the Webhook Relay endpoint is stable, you can stop and restart the agent, reboot your machine, or come back next week — the Plaid configuration never needs to change. ## [Get started](#get-started) 1. Inspect the real payload in the free [Webhook Bin](https://webhookrelay.com/blog/receive-plaid-webhooks-locally/webhook-bin/) — no signup needed. 2. [Create a Webhook Relay account](https://my.webhookrelay.com/register), install the agent, and run `relay forward --bucket plaid http://localhost:8080/webhook`. 3. Point your Plaid webhook at the stable endpoint, trigger an event, and watch it hit `localhost`. You will be testing real Plaid events against your local handler in a few minutes — no deploys, no open firewall ports, and a URL you configure exactly once. --- --- title: Test DocuSign Webhooks Locally | WebhookRelay meta: "og: title": "Test DocuSign Webhooks Locally" description: Test DocuSign webhooks locally and receive them on localhost without deploying. Inspect the real payload, forward to your handler, and verify the signature. url: https://webhookrelay.com/blog/receive-docusign-webhooks-locally.md file: /blog/receive-docusign-webhooks-locally.md --- ![Stripes](https://webhookrelay.com/blog/receive-docusign-webhooks-locally/images/stripes.svg) # **Test DocuSign Webhooks Locally** Test DocuSign webhooks locally and receive them on localhost without deploying. Inspect the real payload, forward to your handler, and verify the signature. ![Test DocuSign Webhooks Locally (Receive DocuSign Webhooks on localhost)](https://webhookrelay.com/blog/receive-docusign-webhooks-locally/images/blog/heroes/localhost.jpg) You are building a DocuSign integration and you need to watch your handler react to a real event. The problem hits immediately: DocuSign will only POST to a **public URL**, and your handler is running on `localhost:8080`. DocuSign has no way to reach it. The usual workarounds are slow. Deploying to a staging environment for every code change kills your iteration speed. Copying a sample payload out of the docs into curl gives you a _guess_ at the real request, not the exact headers and body DocuSign actually sends. What you really want is to **test DocuSign webhooks locally** — real events, hitting your local handler, on a URL that does not change every time you restart. This guide shows how to do exactly that. ## [Why testing DocuSign webhooks locally is tricky](#why-testing-docusign-webhooks-locally-is-tricky) A webhook is just an HTTP request that DocuSign sends to a URL when something changes. DocuSign lives on the public internet; your dev machine usually does not. It sits behind a router, a corporate firewall, or both, with no public IP and no inbound ports open. So you need something in the middle: a public endpoint DocuSign can hit that relays each request down to your laptop without you opening a single firewall port. That is what [Webhook Relay](https://my.webhookrelay.com/register) does — and unlike a random tunnel URL, the endpoint is **stable**, so you configure DocuSign once and never touch it again. ## [Step 1: Inspect the real payload with Webhook Bin](#step-1-inspect-the-real-payload-with-webhook-bin) Before you write any handler code, find out what DocuSign actually sends. Open the free [Webhook Bin](https://webhookrelay.com/blog/receive-docusign-webhooks-locally/webhook-bin/) — no signup — and you get an instant public URL. 1. Copy the Webhook Bin URL. 2. In DocuSign admin, go to **Settings → Connect**, add a **Custom** configuration, paste the URL, choose the envelope/recipient events, and enable HMAC. 3. Trigger a real event and inspect the captured request. You will see the full body and every header. DocuSign Connect can send **JSON or XML** (you choose). The JSON payload includes the event, the envelope status and recipient details. Pick JSON unless you have a reason to parse XML. Now you know the exact shape of the data before writing a line of code. For more on this approach, see [How to test webhooks](https://webhookrelay.com/blog/receive-docusign-webhooks-locally/blog/how-to-test-webhooks/) and [What is a webhook](https://webhookrelay.com/blog/receive-docusign-webhooks-locally/blog/what-is-webhook/). ## [Step 2: Forward the events to localhost with the relay agent](#step-2-forward-the-events-to-localhost-with-the-relay-agent) Once you know the payload, route those same events into your local handler. [Sign up for Webhook Relay](https://my.webhookrelay.com/register), install the `relay` agent (CLI or Docker), and create a bucket — say `docusign`. The bucket gives you a stable public input endpoint. Start forwarding to your local server: ``` relay forward --bucket docusign http://localhost:8080/webhook ``` The agent opens an **outbound** connection to Webhook Relay and streams every incoming request down to `http://localhost:8080/webhook`. Because the connection is outbound, there are **no firewall ports to open and no public IP needed** — this works from your laptop, behind a corporate proxy, or inside a Kubernetes cluster. Running in Docker? The same command works in the official `webhookrelay/webhookrelayd` image. Full details are in the [localhost forwarding docs](https://webhookrelay.com/blog/receive-docusign-webhooks-locally/docs/webhooks/internal/localhost/). Now point the DocuSign webhook at your Webhook Relay endpoint (or create it there from the start), trigger an event, and watch it arrive on `localhost`. ## [DocuSign-specific configuration and quirks](#docusign-specific-configuration-and-quirks) A few DocuSign details worth knowing: - **Where to add it:** **Settings → Connect** in DocuSign admin — add a **Custom** configuration with your URL. - **Enable HMAC:** signing is opt-in. Turn on HMAC and store the key so you can verify deliveries. - **JSON or XML:** Connect can POST either format — choose **JSON** for easier parsing. - **Events:** envelope and recipient lifecycle events; subscribe only to the ones your flow needs. ## [Step 3: Verify the DocuSign webhook signature](#step-3-verify-the-docusign-webhook-signature) DocuSign signs deliveries when you enable **HMAC** on the Connect configuration. It computes an **HMAC-SHA256** of the raw request body using your Connect secret, base64-encodes it, and sends it in the **`X-DocuSign-Signature-1`** header (additional numbered headers appear if you configure multiple keys for rotation). Recompute the HMAC over the raw body and compare against each provided signature header in constant time. To sanity-check an HMAC implementation, paste a captured body, your secret, and the received signature into the free [DocuSign signature verifier](https://webhookrelay.com/blog/receive-docusign-webhooks-locally/verify-docusign-webhook-signature/). For language-specific code and the common pitfalls (reading the body _after_ a JSON parser has already consumed it, timing-safe comparison), read [Verify a webhook signature](https://webhookrelay.com/blog/receive-docusign-webhooks-locally/blog/verify-webhook-signature/). ## [Replay and iterate](#replay-and-iterate) This is where local development gets fast: - **Replay from Webhook Relay** — past requests are stored on your bucket, so you can resend a captured event against your handler without touching DocuSign at all. - **Iterate on your handler** by editing code and replaying the same delivery until it behaves correctly. No commits, no pushes, no deploys just to test a single code path. - **Keep `relay forward` running** while you work — events stream straight to `localhost` as you trigger them in DocuSign. Because the Webhook Relay endpoint is stable, you can stop and restart the agent, reboot your machine, or come back next week — the DocuSign configuration never needs to change. ## [Get started](#get-started) 1. Inspect the real payload in the free [Webhook Bin](https://webhookrelay.com/blog/receive-docusign-webhooks-locally/webhook-bin/) — no signup needed. 2. [Create a Webhook Relay account](https://my.webhookrelay.com/register), install the agent, and run `relay forward --bucket docusign http://localhost:8080/webhook`. 3. Point your DocuSign webhook at the stable endpoint, trigger an event, and watch it hit `localhost`. You will be testing real DocuSign events against your local handler in a few minutes — no deploys, no open firewall ports, and a URL you configure exactly once. --- --- title: Test Zoom Webhooks Locally | WebhookRelay meta: "og: title": "Test Zoom Webhooks Locally" description: Test Zoom webhooks locally and receive them on localhost without deploying. Inspect the real payload, forward to your handler, and verify the signature. url: https://webhookrelay.com/blog/receive-zoom-webhooks-locally.md file: /blog/receive-zoom-webhooks-locally.md --- ![Stripes](https://webhookrelay.com/blog/receive-zoom-webhooks-locally/images/stripes.svg) # **Test Zoom Webhooks Locally** Test Zoom webhooks locally and receive them on localhost without deploying. Inspect the real payload, forward to your handler, and verify the signature. ![Test Zoom Webhooks Locally (Receive Zoom Webhooks on localhost)](https://webhookrelay.com/blog/receive-zoom-webhooks-locally/images/blog/heroes/localhost.jpg) You are building a Zoom integration and you need to watch your handler react to a real event. The problem hits immediately: Zoom will only POST to a **public URL**, and your handler is running on `localhost:8080`. Zoom has no way to reach it. The usual workarounds are slow. Deploying to a staging environment for every code change kills your iteration speed. Copying a sample payload out of the docs into curl gives you a _guess_ at the real request, not the exact headers and body Zoom actually sends. What you really want is to **test Zoom webhooks locally** — real events, hitting your local handler, on a URL that does not change every time you restart. This guide shows how to do exactly that. ## [Why testing Zoom webhooks locally is tricky](#why-testing-zoom-webhooks-locally-is-tricky) A webhook is just an HTTP request that Zoom sends to a URL when something changes. Zoom lives on the public internet; your dev machine usually does not. It sits behind a router, a corporate firewall, or both, with no public IP and no inbound ports open. So you need something in the middle: a public endpoint Zoom can hit that relays each request down to your laptop without you opening a single firewall port. That is what [Webhook Relay](https://my.webhookrelay.com/register) does — and unlike a random tunnel URL, the endpoint is **stable**, so you configure Zoom once and never touch it again. ## [Step 1: Inspect the real payload with Webhook Bin](#step-1-inspect-the-real-payload-with-webhook-bin) Before you write any handler code, find out what Zoom actually sends. Open the free [Webhook Bin](https://webhookrelay.com/blog/receive-zoom-webhooks-locally/webhook-bin/) — no signup — and you get an instant public URL. 1. Copy the Webhook Bin URL. 2. In the Zoom App Marketplace, open your app's **Feature → Event Subscriptions**, add a subscription, paste the URL, and select the events. 3. Trigger a real event and inspect the captured request. You will see the full body and every header. The body is JSON with `event`, a `payload` object (the meeting/recording/user that changed) and an `event_ts`. Before any events arrive, Zoom sends a one-time `endpoint.url_validation` event you must answer. Now you know the exact shape of the data before writing a line of code. For more on this approach, see [How to test webhooks](https://webhookrelay.com/blog/receive-zoom-webhooks-locally/blog/how-to-test-webhooks/) and [What is a webhook](https://webhookrelay.com/blog/receive-zoom-webhooks-locally/blog/what-is-webhook/). ## [Step 2: Forward the events to localhost with the relay agent](#step-2-forward-the-events-to-localhost-with-the-relay-agent) Once you know the payload, route those same events into your local handler. [Sign up for Webhook Relay](https://my.webhookrelay.com/register), install the `relay` agent (CLI or Docker), and create a bucket — say `zoom`. The bucket gives you a stable public input endpoint. Start forwarding to your local server: ``` relay forward --bucket zoom http://localhost:8080/webhook ``` The agent opens an **outbound** connection to Webhook Relay and streams every incoming request down to `http://localhost:8080/webhook`. Because the connection is outbound, there are **no firewall ports to open and no public IP needed** — this works from your laptop, behind a corporate proxy, or inside a Kubernetes cluster. Running in Docker? The same command works in the official `webhookrelay/webhookrelayd` image. Full details are in the [localhost forwarding docs](https://webhookrelay.com/blog/receive-zoom-webhooks-locally/docs/webhooks/internal/localhost/). Now point the Zoom webhook at your Webhook Relay endpoint (or create it there from the start), trigger an event, and watch it arrive on `localhost`. ## [Zoom-specific configuration and quirks](#zoom-specific-configuration-and-quirks) A few Zoom details worth knowing: - **Where to add it:** the Zoom App Marketplace → your app → **Feature → Event Subscriptions**. - **URL validation:** Zoom first sends an `endpoint.url_validation` event — respond with `plainToken` + `encryptedToken` or the subscription won't activate. Capturing it in a bin shows you the exact value to echo. - **Events:** meetings, webinars, recordings, users and more — subscribe to what you need. - **Signature:** `v0=` HMAC-SHA256 like Slack — see the verification step. ## [Step 3: Verify the Zoom webhook signature](#step-3-verify-the-zoom-webhook-signature) Zoom signs every request. It builds the string `"v0:{x-zm-request-timestamp}:{raw body}"`, computes an **HMAC-SHA256** with your app's **Secret Token**, and sends it as `v0=` in the **`x-zm-signature`** header. Recompute it and compare in constant time. Note the one-time **`endpoint.url_validation`** handshake: when Zoom sends it, you must respond with a `plainToken` and its `encryptedToken` (HMAC-SHA256 of the plainToken with the Secret Token) to validate the endpoint. To sanity-check an HMAC implementation, paste a captured body, your secret, and the received signature into the free [HMAC signature verifier](https://webhookrelay.com/blog/receive-zoom-webhooks-locally/hmac-verification/). For language-specific code and the common pitfalls (reading the body _after_ a JSON parser has already consumed it, timing-safe comparison), read [Verify a webhook signature](https://webhookrelay.com/blog/receive-zoom-webhooks-locally/blog/verify-webhook-signature/). ## [Replay and iterate](#replay-and-iterate) This is where local development gets fast: - **Replay from Webhook Relay** — past requests are stored on your bucket, so you can resend a captured event against your handler without touching Zoom at all. - **Iterate on your handler** by editing code and replaying the same delivery until it behaves correctly. No commits, no pushes, no deploys just to test a single code path. - **Keep `relay forward` running** while you work — events stream straight to `localhost` as you trigger them in Zoom. Because the Webhook Relay endpoint is stable, you can stop and restart the agent, reboot your machine, or come back next week — the Zoom configuration never needs to change. ## [Get started](#get-started) 1. Inspect the real payload in the free [Webhook Bin](https://webhookrelay.com/blog/receive-zoom-webhooks-locally/webhook-bin/) — no signup needed. 2. [Create a Webhook Relay account](https://my.webhookrelay.com/register), install the agent, and run `relay forward --bucket zoom http://localhost:8080/webhook`. 3. Point your Zoom webhook at the stable endpoint, trigger an event, and watch it hit `localhost`. You will be testing real Zoom events against your local handler in a few minutes — no deploys, no open firewall ports, and a URL you configure exactly once. --- --- title: Test Jira Webhooks Locally | WebhookRelay meta: "og: title": "Test Jira Webhooks Locally" description: Test Jira webhooks locally and receive them on localhost without deploying. Inspect the real payload, forward to your handler, and verify the signature. url: https://webhookrelay.com/blog/receive-jira-webhooks-locally.md file: /blog/receive-jira-webhooks-locally.md --- ![Stripes](https://webhookrelay.com/blog/receive-jira-webhooks-locally/images/stripes.svg) # **Test Jira Webhooks Locally** Test Jira webhooks locally and receive them on localhost without deploying. Inspect the real payload, forward to your handler, and verify the signature. ![Test Jira Webhooks Locally (Receive Jira Webhooks on localhost)](https://webhookrelay.com/blog/receive-jira-webhooks-locally/images/blog/heroes/localhost.jpg) You are building a Jira integration and you need to watch your handler react to a real event. The problem hits immediately: Jira will only POST to a **public URL**, and your handler is running on `localhost:8080`. Jira has no way to reach it. The usual workarounds are slow. Deploying to a staging environment for every code change kills your iteration speed. Copying a sample payload out of the docs into curl gives you a _guess_ at the real request, not the exact headers and body Jira actually sends. What you really want is to **test Jira webhooks locally** — real events, hitting your local handler, on a URL that does not change every time you restart. This guide shows how to do exactly that. ## [Why testing Jira webhooks locally is tricky](#why-testing-jira-webhooks-locally-is-tricky) A webhook is just an HTTP request that Jira sends to a URL when something changes. Jira lives on the public internet; your dev machine usually does not. It sits behind a router, a corporate firewall, or both, with no public IP and no inbound ports open. So you need something in the middle: a public endpoint Jira can hit that relays each request down to your laptop without you opening a single firewall port. That is what [Webhook Relay](https://my.webhookrelay.com/register) does — and unlike a random tunnel URL, the endpoint is **stable**, so you configure Jira once and never touch it again. ## [Step 1: Inspect the real payload with Webhook Bin](#step-1-inspect-the-real-payload-with-webhook-bin) Before you write any handler code, find out what Jira actually sends. Open the free [Webhook Bin](https://webhookrelay.com/blog/receive-jira-webhooks-locally/webhook-bin/) — no signup — and you get an instant public URL. 1. Copy the Webhook Bin URL. 2. In Jira, go to **Settings → System → Webhooks → Create a WebHook**, paste the URL, and pick the events and a JQL filter. For signed deliveries, register a **dynamic** webhook via the REST API with a `secret`. 3. Trigger a real event and inspect the captured request. You will see the full body and every header. The body is JSON with a `webhookEvent` field plus the changed `issue`, `comment` or `user` objects and a `changelog` for updates. You can also narrow deliveries with a JQL filter when you create the webhook. Now you know the exact shape of the data before writing a line of code. For more on this approach, see [How to test webhooks](https://webhookrelay.com/blog/receive-jira-webhooks-locally/blog/how-to-test-webhooks/) and [What is a webhook](https://webhookrelay.com/blog/receive-jira-webhooks-locally/blog/what-is-webhook/). ## [Step 2: Forward the events to localhost with the relay agent](#step-2-forward-the-events-to-localhost-with-the-relay-agent) Once you know the payload, route those same events into your local handler. [Sign up for Webhook Relay](https://my.webhookrelay.com/register), install the `relay` agent (CLI or Docker), and create a bucket — say `jira`. The bucket gives you a stable public input endpoint. Start forwarding to your local server: ``` relay forward --bucket jira http://localhost:8080/webhook ``` The agent opens an **outbound** connection to Webhook Relay and streams every incoming request down to `http://localhost:8080/webhook`. Because the connection is outbound, there are **no firewall ports to open and no public IP needed** — this works from your laptop, behind a corporate proxy, or inside a Kubernetes cluster. Running in Docker? The same command works in the official `webhookrelay/webhookrelayd` image. Full details are in the [localhost forwarding docs](https://webhookrelay.com/blog/receive-jira-webhooks-locally/docs/webhooks/internal/localhost/). Now point the Jira webhook at your Webhook Relay endpoint (or create it there from the start), trigger an event, and watch it arrive on `localhost`. ## [Jira-specific configuration and quirks](#jira-specific-configuration-and-quirks) A few Jira details worth knowing: - **Two ways to register:** **Settings → System → Webhooks** in the admin UI, or the REST API for **dynamic** webhooks (which support a signing `secret`). - **JQL filter:** scope deliveries to specific projects or issue types with a JQL filter so you don't get the whole instance's traffic. - **Payload shape:** JSON with `webhookEvent`, the changed entity, and a `changelog` on updates. - **Signing varies:** dynamic webhooks sign with `X-Hub-Signature`; classic ones don't — see the verification step. ## [Step 3: Verify the Jira webhook signature](#step-3-verify-the-jira-webhook-signature) How you verify depends on how the webhook was created. If you register a **dynamic webhook** through Jira's REST API with a `secret`, Jira signs the raw body with **HMAC-SHA256** and sends it as `sha256=...` in the **`X-Hub-Signature`** header — recompute and compare in constant time. Classic webhooks created in the admin UI are **not** signed; for those, put a hard-to-guess secret in the webhook URL and check it, and/or restrict inbound traffic to Atlassian's IP ranges. To sanity-check an HMAC implementation, paste a captured body, your secret, and the received signature into the free [HMAC signature verifier](https://webhookrelay.com/blog/receive-jira-webhooks-locally/hmac-verification/). For language-specific code and the common pitfalls (reading the body _after_ a JSON parser has already consumed it, timing-safe comparison), read [Verify a webhook signature](https://webhookrelay.com/blog/receive-jira-webhooks-locally/blog/verify-webhook-signature/). ## [Replay and iterate](#replay-and-iterate) This is where local development gets fast: - **Replay from Webhook Relay** — past requests are stored on your bucket, so you can resend a captured event against your handler without touching Jira at all. - **Iterate on your handler** by editing code and replaying the same delivery until it behaves correctly. No commits, no pushes, no deploys just to test a single code path. - **Keep `relay forward` running** while you work — events stream straight to `localhost` as you trigger them in Jira. Because the Webhook Relay endpoint is stable, you can stop and restart the agent, reboot your machine, or come back next week — the Jira configuration never needs to change. ## [Get started](#get-started) 1. Inspect the real payload in the free [Webhook Bin](https://webhookrelay.com/blog/receive-jira-webhooks-locally/webhook-bin/) — no signup needed. 2. [Create a Webhook Relay account](https://my.webhookrelay.com/register), install the agent, and run `relay forward --bucket jira http://localhost:8080/webhook`. 3. Point your Jira webhook at the stable endpoint, trigger an event, and watch it hit `localhost`. You will be testing real Jira events against your local handler in a few minutes — no deploys, no open firewall ports, and a URL you configure exactly once. --- --- title: Test Mailchimp Webhooks Locally | WebhookRelay meta: "og: title": "Test Mailchimp Webhooks Locally" description: Test Mailchimp webhooks locally and receive them on localhost without deploying. Inspect the real payload, forward to your handler, and verify the signature. url: https://webhookrelay.com/blog/receive-mailchimp-webhooks-locally.md file: /blog/receive-mailchimp-webhooks-locally.md --- ![Stripes](https://webhookrelay.com/blog/receive-mailchimp-webhooks-locally/images/stripes.svg) # **Test Mailchimp Webhooks Locally** Test Mailchimp webhooks locally and receive them on localhost without deploying. Inspect the real payload, forward to your handler, and verify the signature. ![Test Mailchimp Webhooks Locally (Receive Mailchimp Webhooks on localhost)](https://webhookrelay.com/blog/receive-mailchimp-webhooks-locally/images/blog/heroes/localhost.jpg) You are building a Mailchimp integration and you need to watch your handler react to a real event. The problem hits immediately: Mailchimp will only POST to a **public URL**, and your handler is running on `localhost:8080`. Mailchimp has no way to reach it. The usual workarounds are slow. Deploying to a staging environment for every code change kills your iteration speed. Copying a sample payload out of the docs into curl gives you a _guess_ at the real request, not the exact headers and body Mailchimp actually sends. What you really want is to **test Mailchimp webhooks locally** — real events, hitting your local handler, on a URL that does not change every time you restart. This guide shows how to do exactly that. ## [Why testing Mailchimp webhooks locally is tricky](#why-testing-mailchimp-webhooks-locally-is-tricky) A webhook is just an HTTP request that Mailchimp sends to a URL when something changes. Mailchimp lives on the public internet; your dev machine usually does not. It sits behind a router, a corporate firewall, or both, with no public IP and no inbound ports open. So you need something in the middle: a public endpoint Mailchimp can hit that relays each request down to your laptop without you opening a single firewall port. That is what [Webhook Relay](https://my.webhookrelay.com/register) does — and unlike a random tunnel URL, the endpoint is **stable**, so you configure Mailchimp once and never touch it again. ## [Step 1: Inspect the real payload with Webhook Bin](#step-1-inspect-the-real-payload-with-webhook-bin) Before you write any handler code, find out what Mailchimp actually sends. Open the free [Webhook Bin](https://webhookrelay.com/blog/receive-mailchimp-webhooks-locally/webhook-bin/) — no signup — and you get an instant public URL. 1. Copy the Webhook Bin URL. 2. In Mailchimp, go to **Audience → Settings → Webhooks**, click **Create New Webhook**, paste the URL, and choose the events. 3. Trigger a real event and inspect the captured request. You will see the full body and every header. Mailchimp marketing webhooks are **`application/x-www-form-urlencoded`**, not JSON. You get a `type` (e.g. `subscribe`) and a `data[...]` map (email, list id, merge fields). Parse it as form data, not JSON. Now you know the exact shape of the data before writing a line of code. For more on this approach, see [How to test webhooks](https://webhookrelay.com/blog/receive-mailchimp-webhooks-locally/blog/how-to-test-webhooks/) and [What is a webhook](https://webhookrelay.com/blog/receive-mailchimp-webhooks-locally/blog/what-is-webhook/). ## [Step 2: Forward the events to localhost with the relay agent](#step-2-forward-the-events-to-localhost-with-the-relay-agent) Once you know the payload, route those same events into your local handler. [Sign up for Webhook Relay](https://my.webhookrelay.com/register), install the `relay` agent (CLI or Docker), and create a bucket — say `mailchimp`. The bucket gives you a stable public input endpoint. Start forwarding to your local server: ``` relay forward --bucket mailchimp http://localhost:8080/webhook ``` The agent opens an **outbound** connection to Webhook Relay and streams every incoming request down to `http://localhost:8080/webhook`. Because the connection is outbound, there are **no firewall ports to open and no public IP needed** — this works from your laptop, behind a corporate proxy, or inside a Kubernetes cluster. Running in Docker? The same command works in the official `webhookrelay/webhookrelayd` image. Full details are in the [localhost forwarding docs](https://webhookrelay.com/blog/receive-mailchimp-webhooks-locally/docs/webhooks/internal/localhost/). Now point the Mailchimp webhook at your Webhook Relay endpoint (or create it there from the start), trigger an event, and watch it arrive on `localhost`. ## [Mailchimp-specific configuration and quirks](#mailchimp-specific-configuration-and-quirks) A few Mailchimp details worth knowing: - **Where to add it:** **Audience → Settings → Webhooks**. - **Form-encoded:** the body is `application/x-www-form-urlencoded` with a `type` and `data[...]` map — parse it as form data, not JSON. - **No HMAC:** marketing webhooks aren't signed — put a secret in the URL and check it. Mandrill (transactional) signs separately. - **Verify endpoint:** Mailchimp first sends a `GET` to confirm the URL exists, then POSTs events. ## [Step 3: Verify the Mailchimp webhook signature](#step-3-verify-the-mailchimp-webhook-signature) Mailchimp's marketing webhooks are **not** signed with an HMAC, so there's nothing to recompute. Instead, secure the endpoint by putting a secret in the webhook **URL** query string (for example `?key=long-random-value`) and rejecting any request that doesn't carry it. If you use **Mandrill** (transactional email), that product _does_ sign — it sends an **`X-Mandrill-Signature`** (HMAC-SHA1, base64) computed over the webhook URL plus the sorted POST parameters, which you verify with your Mandrill webhook key. To sanity-check an HMAC implementation, paste a captured body, your secret, and the received signature into the free [HMAC signature verifier](https://webhookrelay.com/blog/receive-mailchimp-webhooks-locally/hmac-verification/). For language-specific code and the common pitfalls (reading the body _after_ a JSON parser has already consumed it, timing-safe comparison), read [Verify a webhook signature](https://webhookrelay.com/blog/receive-mailchimp-webhooks-locally/blog/verify-webhook-signature/). ## [Replay and iterate](#replay-and-iterate) This is where local development gets fast: - **Replay from Webhook Relay** — past requests are stored on your bucket, so you can resend a captured event against your handler without touching Mailchimp at all. - **Iterate on your handler** by editing code and replaying the same delivery until it behaves correctly. No commits, no pushes, no deploys just to test a single code path. - **Keep `relay forward` running** while you work — events stream straight to `localhost` as you trigger them in Mailchimp. Because the Webhook Relay endpoint is stable, you can stop and restart the agent, reboot your machine, or come back next week — the Mailchimp configuration never needs to change. ## [Get started](#get-started) 1. Inspect the real payload in the free [Webhook Bin](https://webhookrelay.com/blog/receive-mailchimp-webhooks-locally/webhook-bin/) — no signup needed. 2. [Create a Webhook Relay account](https://my.webhookrelay.com/register), install the agent, and run `relay forward --bucket mailchimp http://localhost:8080/webhook`. 3. Point your Mailchimp webhook at the stable endpoint, trigger an event, and watch it hit `localhost`. You will be testing real Mailchimp events against your local handler in a few minutes — no deploys, no open firewall ports, and a URL you configure exactly once. --- --- title: Test Adyen Webhooks Locally | WebhookRelay meta: "og: title": "Test Adyen Webhooks Locally" description: Test Adyen webhooks locally and receive them on localhost without deploying. Inspect the real payload, forward to your handler, and verify the signature. url: https://webhookrelay.com/blog/receive-adyen-webhooks-locally.md file: /blog/receive-adyen-webhooks-locally.md --- ![Stripes](https://webhookrelay.com/blog/receive-adyen-webhooks-locally/images/stripes.svg) # **Test Adyen Webhooks Locally** Test Adyen webhooks locally and receive them on localhost without deploying. Inspect the real payload, forward to your handler, and verify the signature. ![Test Adyen Webhooks Locally (Receive Adyen Webhooks on localhost)](https://webhookrelay.com/blog/receive-adyen-webhooks-locally/images/blog/heroes/localhost.jpg) You are building a Adyen integration and you need to watch your handler react to a real event. The problem hits immediately: Adyen will only POST to a **public URL**, and your handler is running on `localhost:8080`. Adyen has no way to reach it. The usual workarounds are slow. Deploying to a staging environment for every code change kills your iteration speed. Copying a sample payload out of the docs into curl gives you a _guess_ at the real request, not the exact headers and body Adyen actually sends. What you really want is to **test Adyen webhooks locally** — real events, hitting your local handler, on a URL that does not change every time you restart. This guide shows how to do exactly that. ## [Why testing Adyen webhooks locally is tricky](#why-testing-adyen-webhooks-locally-is-tricky) A webhook is just an HTTP request that Adyen sends to a URL when something changes. Adyen lives on the public internet; your dev machine usually does not. It sits behind a router, a corporate firewall, or both, with no public IP and no inbound ports open. So you need something in the middle: a public endpoint Adyen can hit that relays each request down to your laptop without you opening a single firewall port. That is what [Webhook Relay](https://my.webhookrelay.com/register) does — and unlike a random tunnel URL, the endpoint is **stable**, so you configure Adyen once and never touch it again. ## [Step 1: Inspect the real payload with Webhook Bin](#step-1-inspect-the-real-payload-with-webhook-bin) Before you write any handler code, find out what Adyen actually sends. Open the free [Webhook Bin](https://webhookrelay.com/blog/receive-adyen-webhooks-locally/webhook-bin/) — no signup — and you get an instant public URL. 1. Copy the Webhook Bin URL. 2. In the Adyen Customer Area, go to **Developers → Webhooks**, add a **Standard webhook**, paste the URL, and generate an **HMAC key**. 3. Trigger a real event and inspect the captured request. You will see the full body and every header. Adyen batches events into a `notificationItems` array — each item is a `NotificationRequestItem` with the event code, PSP reference, amount and an `additionalData.hmacSignature`. Your endpoint must respond with the body `[accepted]` so Adyen stops retrying. Now you know the exact shape of the data before writing a line of code. For more on this approach, see [How to test webhooks](https://webhookrelay.com/blog/receive-adyen-webhooks-locally/blog/how-to-test-webhooks/) and [What is a webhook](https://webhookrelay.com/blog/receive-adyen-webhooks-locally/blog/what-is-webhook/). ## [Step 2: Forward the events to localhost with the relay agent](#step-2-forward-the-events-to-localhost-with-the-relay-agent) Once you know the payload, route those same events into your local handler. [Sign up for Webhook Relay](https://my.webhookrelay.com/register), install the `relay` agent (CLI or Docker), and create a bucket — say `adyen`. The bucket gives you a stable public input endpoint. Start forwarding to your local server: ``` relay forward --bucket adyen http://localhost:8080/webhook ``` The agent opens an **outbound** connection to Webhook Relay and streams every incoming request down to `http://localhost:8080/webhook`. Because the connection is outbound, there are **no firewall ports to open and no public IP needed** — this works from your laptop, behind a corporate proxy, or inside a Kubernetes cluster. Running in Docker? The same command works in the official `webhookrelay/webhookrelayd` image. Full details are in the [localhost forwarding docs](https://webhookrelay.com/blog/receive-adyen-webhooks-locally/docs/webhooks/internal/localhost/). Now point the Adyen webhook at your Webhook Relay endpoint (or create it there from the start), trigger an event, and watch it arrive on `localhost`. ## [Adyen-specific configuration and quirks](#adyen-specific-configuration-and-quirks) A few Adyen details worth knowing: - **Where to add it:** Customer Area → **Developers → Webhooks** → Standard webhook. Generate the **HMAC key** there. - **Batched:** events arrive in a `notificationItems` array — loop over them. Always respond with `[accepted]`. - **Basic Auth:** set username/password on the webhook and check them on your endpoint. - **Test:** the Customer Area can send test notifications straight to your bin. ## [Step 3: Verify the Adyen webhook signature](#step-3-verify-the-adyen-webhook-signature) Adyen signs each notification item with an **HMAC-SHA256** placed in `additionalData.hmacSignature` (base64), computed over a defined, ordered set of fields (merchant account, PSP reference, original/value amount, event code, success…) using the **HMAC key** you generated in the Customer Area. Rebuild that string exactly, compute the HMAC, base64-encode, and compare. Adyen also protects the endpoint with **Basic Auth**, so set and check those credentials too. To sanity-check an HMAC implementation, paste a captured body, your secret, and the received signature into the free [HMAC signature verifier](https://webhookrelay.com/blog/receive-adyen-webhooks-locally/hmac-verification/). For language-specific code and the common pitfalls (reading the body _after_ a JSON parser has already consumed it, timing-safe comparison), read [Verify a webhook signature](https://webhookrelay.com/blog/receive-adyen-webhooks-locally/blog/verify-webhook-signature/). ## [Replay and iterate](#replay-and-iterate) This is where local development gets fast: - **Replay from Webhook Relay** — past requests are stored on your bucket, so you can resend a captured event against your handler without touching Adyen at all. - **Iterate on your handler** by editing code and replaying the same delivery until it behaves correctly. No commits, no pushes, no deploys just to test a single code path. - **Keep `relay forward` running** while you work — events stream straight to `localhost` as you trigger them in Adyen. Because the Webhook Relay endpoint is stable, you can stop and restart the agent, reboot your machine, or come back next week — the Adyen configuration never needs to change. ## [Get started](#get-started) 1. Inspect the real payload in the free [Webhook Bin](https://webhookrelay.com/blog/receive-adyen-webhooks-locally/webhook-bin/) — no signup needed. 2. [Create a Webhook Relay account](https://my.webhookrelay.com/register), install the agent, and run `relay forward --bucket adyen http://localhost:8080/webhook`. 3. Point your Adyen webhook at the stable endpoint, trigger an event, and watch it hit `localhost`. You will be testing real Adyen events against your local handler in a few minutes — no deploys, no open firewall ports, and a URL you configure exactly once. --- --- title: Test Zendesk Webhooks Locally | WebhookRelay meta: "og: title": "Test Zendesk Webhooks Locally" description: Test Zendesk webhooks locally and receive them on localhost without deploying. Inspect the real payload, forward to your handler, and verify the signature. url: https://webhookrelay.com/blog/receive-zendesk-webhooks-locally.md file: /blog/receive-zendesk-webhooks-locally.md --- ![Stripes](https://webhookrelay.com/blog/receive-zendesk-webhooks-locally/images/stripes.svg) # **Test Zendesk Webhooks Locally** Test Zendesk webhooks locally and receive them on localhost without deploying. Inspect the real payload, forward to your handler, and verify the signature. ![Test Zendesk Webhooks Locally (Receive Zendesk Webhooks on localhost)](https://webhookrelay.com/blog/receive-zendesk-webhooks-locally/images/blog/heroes/localhost.jpg) You are building a Zendesk integration and you need to watch your handler react to a real event. The problem hits immediately: Zendesk will only POST to a **public URL**, and your handler is running on `localhost:8080`. Zendesk has no way to reach it. The usual workarounds are slow. Deploying to a staging environment for every code change kills your iteration speed. Copying a sample payload out of the docs into curl gives you a _guess_ at the real request, not the exact headers and body Zendesk actually sends. What you really want is to **test Zendesk webhooks locally** — real events, hitting your local handler, on a URL that does not change every time you restart. This guide shows how to do exactly that. ## [Why testing Zendesk webhooks locally is tricky](#why-testing-zendesk-webhooks-locally-is-tricky) A webhook is just an HTTP request that Zendesk sends to a URL when something changes. Zendesk lives on the public internet; your dev machine usually does not. It sits behind a router, a corporate firewall, or both, with no public IP and no inbound ports open. So you need something in the middle: a public endpoint Zendesk can hit that relays each request down to your laptop without you opening a single firewall port. That is what [Webhook Relay](https://my.webhookrelay.com/register) does — and unlike a random tunnel URL, the endpoint is **stable**, so you configure Zendesk once and never touch it again. ## [Step 1: Inspect the real payload with Webhook Bin](#step-1-inspect-the-real-payload-with-webhook-bin) Before you write any handler code, find out what Zendesk actually sends. Open the free [Webhook Bin](https://webhookrelay.com/blog/receive-zendesk-webhooks-locally/webhook-bin/) — no signup — and you get an instant public URL. 1. Copy the Webhook Bin URL. 2. In Zendesk Admin Center, go to **Apps and integrations → Webhooks → Create webhook**, set the URL, turn on **signing**, then connect it to a trigger or automation. 3. Trigger a real event and inspect the captured request. You will see the full body and every header. A Zendesk webhook sends whatever JSON you define in the connected trigger. With signing on, each request includes `X-Zendesk-Webhook-Signature` and `X-Zendesk-Webhook-Signature-Timestamp` headers. Now you know the exact shape of the data before writing a line of code. For more on this approach, see [How to test webhooks](https://webhookrelay.com/blog/receive-zendesk-webhooks-locally/blog/how-to-test-webhooks/) and [What is a webhook](https://webhookrelay.com/blog/receive-zendesk-webhooks-locally/blog/what-is-webhook/). ## [Step 2: Forward the events to localhost with the relay agent](#step-2-forward-the-events-to-localhost-with-the-relay-agent) Once you know the payload, route those same events into your local handler. [Sign up for Webhook Relay](https://my.webhookrelay.com/register), install the `relay` agent (CLI or Docker), and create a bucket — say `zendesk`. The bucket gives you a stable public input endpoint. Start forwarding to your local server: ``` relay forward --bucket zendesk http://localhost:8080/webhook ``` The agent opens an **outbound** connection to Webhook Relay and streams every incoming request down to `http://localhost:8080/webhook`. Because the connection is outbound, there are **no firewall ports to open and no public IP needed** — this works from your laptop, behind a corporate proxy, or inside a Kubernetes cluster. Running in Docker? The same command works in the official `webhookrelay/webhookrelayd` image. Full details are in the [localhost forwarding docs](https://webhookrelay.com/blog/receive-zendesk-webhooks-locally/docs/webhooks/internal/localhost/). Now point the Zendesk webhook at your Webhook Relay endpoint (or create it there from the start), trigger an event, and watch it arrive on `localhost`. ## [Zendesk-specific configuration and quirks](#zendesk-specific-configuration-and-quirks) A few Zendesk details worth knowing: - **Where to add it:** **Admin Center → Apps and integrations → Webhooks**; connect it to a **trigger** or **automation** to fire it. - **Custom body:** you template the JSON in the trigger, so the payload shape is whatever you define. - **Signing:** enable it to get the signature headers. ## [Step 3: Verify the Zendesk webhook signature](#step-3-verify-the-zendesk-webhook-signature) Zendesk signs `"{timestamp}{raw body}"` with **HMAC-SHA256** using the webhook's **signing secret**, base64-encodes it, and sends it in the **`X-Zendesk-Webhook-Signature`** header alongside `X-Zendesk-Webhook-Signature-Timestamp`. Rebuild `timestamp + rawBody`, compute the HMAC, base64-encode, and compare in constant time. To sanity-check an HMAC implementation, paste a captured body, your secret, and the received signature into the free [Zendesk signature verifier](https://webhookrelay.com/blog/receive-zendesk-webhooks-locally/verify-zendesk-webhook-signature/). For language-specific code and the common pitfalls (reading the body _after_ a JSON parser has already consumed it, timing-safe comparison), read [Verify a webhook signature](https://webhookrelay.com/blog/receive-zendesk-webhooks-locally/blog/verify-webhook-signature/). ## [Replay and iterate](#replay-and-iterate) This is where local development gets fast: - **Replay from Webhook Relay** — past requests are stored on your bucket, so you can resend a captured event against your handler without touching Zendesk at all. - **Iterate on your handler** by editing code and replaying the same delivery until it behaves correctly. No commits, no pushes, no deploys just to test a single code path. - **Keep `relay forward` running** while you work — events stream straight to `localhost` as you trigger them in Zendesk. Because the Webhook Relay endpoint is stable, you can stop and restart the agent, reboot your machine, or come back next week — the Zendesk configuration never needs to change. ## [Get started](#get-started) 1. Inspect the real payload in the free [Webhook Bin](https://webhookrelay.com/blog/receive-zendesk-webhooks-locally/webhook-bin/) — no signup needed. 2. [Create a Webhook Relay account](https://my.webhookrelay.com/register), install the agent, and run `relay forward --bucket zendesk http://localhost:8080/webhook`. 3. Point your Zendesk webhook at the stable endpoint, trigger an event, and watch it hit `localhost`. You will be testing real Zendesk events against your local handler in a few minutes — no deploys, no open firewall ports, and a URL you configure exactly once. --- --- title: Test Coinbase Commerce Webhooks Locally | WebhookRelay meta: "og: title": "Test Coinbase Commerce Webhooks Locally" description: Test Coinbase Commerce webhooks locally without deploying. Inspect the real payload, forward it to your handler, and verify the X-CC-Webhook-Signature. url: https://webhookrelay.com/blog/receive-coinbase-commerce-webhooks-locally.md file: /blog/receive-coinbase-commerce-webhooks-locally.md --- ![Stripes](https://webhookrelay.com/blog/receive-coinbase-commerce-webhooks-locally/images/stripes.svg) # **Test Coinbase Commerce Webhooks Locally** Test Coinbase Commerce webhooks locally without deploying. Inspect the real payload, forward it to your handler, and verify the X-CC-Webhook-Signature. ![Test Coinbase Commerce Webhooks Locally (Receive Coinbase Commerce Webhooks on localhost)](https://webhookrelay.com/blog/receive-coinbase-commerce-webhooks-locally/images/blog/heroes/localhost.jpg) You are building a Coinbase Commerce integration and you need to watch your handler react to a real event. The problem hits immediately: Coinbase Commerce will only POST to a **public URL**, and your handler is running on `localhost:8080`. Coinbase Commerce has no way to reach it. The usual workarounds are slow. Deploying to a staging environment for every code change kills your iteration speed. Copying a sample payload out of the docs into curl gives you a _guess_ at the real request, not the exact headers and body Coinbase Commerce actually sends. What you really want is to **test Coinbase Commerce webhooks locally** — real events, hitting your local handler, on a URL that does not change every time you restart. This guide shows how to do exactly that. ## [Why testing Coinbase Commerce webhooks locally is tricky](#why-testing-coinbase-commerce-webhooks-locally-is-tricky) A webhook is just an HTTP request that Coinbase Commerce sends to a URL when something changes. Coinbase Commerce lives on the public internet; your dev machine usually does not. It sits behind a router, a corporate firewall, or both, with no public IP and no inbound ports open. So you need something in the middle: a public endpoint Coinbase Commerce can hit that relays each request down to your laptop without you opening a single firewall port. That is what [Webhook Relay](https://my.webhookrelay.com/register) does — and unlike a random tunnel URL, the endpoint is **stable**, so you configure Coinbase Commerce once and never touch it again. ## [Step 1: Inspect the real payload with Webhook Bin](#step-1-inspect-the-real-payload-with-webhook-bin) Before you write any handler code, find out what Coinbase Commerce actually sends. Open the free [Webhook Bin](https://webhookrelay.com/blog/receive-coinbase-commerce-webhooks-locally/webhook-bin/) — no signup — and you get an instant public URL. 1. Copy the Webhook Bin URL. 2. In Coinbase Commerce, go to **Settings → Webhooks**, add an endpoint with the URL, and copy the **shared secret**. 3. Trigger a real event and inspect the captured request. You will see the full body and every header. The body is JSON with an `event` object (`type`, `data`) describing the charge. The signature travels in the `X-CC-Webhook-Signature` header. Now you know the exact shape of the data before writing a line of code. For more on this approach, see [How to test webhooks](https://webhookrelay.com/blog/receive-coinbase-commerce-webhooks-locally/blog/how-to-test-webhooks/) and [What is a webhook](https://webhookrelay.com/blog/receive-coinbase-commerce-webhooks-locally/blog/what-is-webhook/). ## [Step 2: Forward the events to localhost with the relay agent](#step-2-forward-the-events-to-localhost-with-the-relay-agent) Once you know the payload, route those same events into your local handler. [Sign up for Webhook Relay](https://my.webhookrelay.com/register), install the `relay` agent (CLI or Docker), and create a bucket — say `coinbase`. The bucket gives you a stable public input endpoint. Start forwarding to your local server: ``` relay forward --bucket coinbase http://localhost:8080/webhook ``` The agent opens an **outbound** connection to Webhook Relay and streams every incoming request down to `http://localhost:8080/webhook`. Because the connection is outbound, there are **no firewall ports to open and no public IP needed** — this works from your laptop, behind a corporate proxy, or inside a Kubernetes cluster. Running in Docker? The same command works in the official `webhookrelay/webhookrelayd` image. Full details are in the [localhost forwarding docs](https://webhookrelay.com/blog/receive-coinbase-commerce-webhooks-locally/docs/webhooks/internal/localhost/). Now point the Coinbase Commerce webhook at your Webhook Relay endpoint (or create it there from the start), trigger an event, and watch it arrive on `localhost`. ## [Coinbase Commerce-specific configuration and quirks](#coinbase-commerce-specific-configuration-and-quirks) A few Coinbase Commerce details worth knowing: - **Where to add it:** **Settings → Webhooks** — copy the shared secret shown there. - **Events:** `charge:created`, `charge:confirmed`, `charge:failed`, `charge:pending`, `charge:resolved`. - **Raw body:** verify against the unparsed body — re-serialising the JSON changes the bytes and breaks the signature. ## [Step 3: Verify the Coinbase Commerce webhook signature](#step-3-verify-the-coinbase-commerce-webhook-signature) Coinbase Commerce signs the **raw request body** with **HMAC-SHA256** using your endpoint's **shared secret** and sends the hex digest in the **`X-CC-Webhook-Signature`** header. Recompute the HMAC over the exact raw body (before any JSON parsing) and compare in constant time. To sanity-check an HMAC implementation, paste a captured body, your secret, and the received signature into the free [HMAC signature verifier](https://webhookrelay.com/blog/receive-coinbase-commerce-webhooks-locally/hmac-verification/). For language-specific code and the common pitfalls (reading the body _after_ a JSON parser has already consumed it, timing-safe comparison), read [Verify a webhook signature](https://webhookrelay.com/blog/receive-coinbase-commerce-webhooks-locally/blog/verify-webhook-signature/). ## [Replay and iterate](#replay-and-iterate) This is where local development gets fast: - **Replay from Webhook Relay** — past requests are stored on your bucket, so you can resend a captured event against your handler without touching Coinbase Commerce at all. - **Iterate on your handler** by editing code and replaying the same delivery until it behaves correctly. No commits, no pushes, no deploys just to test a single code path. - **Keep `relay forward` running** while you work — events stream straight to `localhost` as you trigger them in Coinbase Commerce. Because the Webhook Relay endpoint is stable, you can stop and restart the agent, reboot your machine, or come back next week — the Coinbase Commerce configuration never needs to change. ## [Get started](#get-started) 1. Inspect the real payload in the free [Webhook Bin](https://webhookrelay.com/blog/receive-coinbase-commerce-webhooks-locally/webhook-bin/) — no signup needed. 2. [Create a Webhook Relay account](https://my.webhookrelay.com/register), install the agent, and run `relay forward --bucket coinbase http://localhost:8080/webhook`. 3. Point your Coinbase Commerce webhook at the stable endpoint, trigger an event, and watch it hit `localhost`. You will be testing real Coinbase Commerce events against your local handler in a few minutes — no deploys, no open firewall ports, and a URL you configure exactly once. --- --- title: Test Asana Webhooks Locally | WebhookRelay meta: "og: title": "Test Asana Webhooks Locally" description: Test Asana webhooks locally and receive them on localhost without deploying. Inspect the real payload, forward to your handler, and verify the signature. url: https://webhookrelay.com/blog/receive-asana-webhooks-locally.md file: /blog/receive-asana-webhooks-locally.md --- ![Stripes](https://webhookrelay.com/blog/receive-asana-webhooks-locally/images/stripes.svg) # **Test Asana Webhooks Locally** Test Asana webhooks locally and receive them on localhost without deploying. Inspect the real payload, forward to your handler, and verify the signature. ![Test Asana Webhooks Locally (Receive Asana Webhooks on localhost)](https://webhookrelay.com/blog/receive-asana-webhooks-locally/images/blog/heroes/localhost.jpg) You are building a Asana integration and you need to watch your handler react to a real event. The problem hits immediately: Asana will only POST to a **public URL**, and your handler is running on `localhost:8080`. Asana has no way to reach it. The usual workarounds are slow. Deploying to a staging environment for every code change kills your iteration speed. Copying a sample payload out of the docs into curl gives you a _guess_ at the real request, not the exact headers and body Asana actually sends. What you really want is to **test Asana webhooks locally** — real events, hitting your local handler, on a URL that does not change every time you restart. This guide shows how to do exactly that. ## [Why testing Asana webhooks locally is tricky](#why-testing-asana-webhooks-locally-is-tricky) A webhook is just an HTTP request that Asana sends to a URL when something changes. Asana lives on the public internet; your dev machine usually does not. It sits behind a router, a corporate firewall, or both, with no public IP and no inbound ports open. So you need something in the middle: a public endpoint Asana can hit that relays each request down to your laptop without you opening a single firewall port. That is what [Webhook Relay](https://my.webhookrelay.com/register) does — and unlike a random tunnel URL, the endpoint is **stable**, so you configure Asana once and never touch it again. ## [Step 1: Inspect the real payload with Webhook Bin](#step-1-inspect-the-real-payload-with-webhook-bin) Before you write any handler code, find out what Asana actually sends. Open the free [Webhook Bin](https://webhookrelay.com/blog/receive-asana-webhooks-locally/webhook-bin/) — no signup — and you get an instant public URL. 1. Copy the Webhook Bin URL. 2. Create the webhook via the Asana API (`POST /webhooks`) with the resource to watch and your URL. Asana immediately sends a handshake you must answer. 3. Trigger a real event and inspect the captured request. You will see the full body and every header. Asana first sends an empty **handshake** request carrying an `X-Hook-Secret` header — echo that header back in your response to confirm the endpoint. After that, each event delivery is signed and the body contains an `events` array describing what changed. Now you know the exact shape of the data before writing a line of code. For more on this approach, see [How to test webhooks](https://webhookrelay.com/blog/receive-asana-webhooks-locally/blog/how-to-test-webhooks/) and [What is a webhook](https://webhookrelay.com/blog/receive-asana-webhooks-locally/blog/what-is-webhook/). ## [Step 2: Forward the events to localhost with the relay agent](#step-2-forward-the-events-to-localhost-with-the-relay-agent) Once you know the payload, route those same events into your local handler. [Sign up for Webhook Relay](https://my.webhookrelay.com/register), install the `relay` agent (CLI or Docker), and create a bucket — say `asana`. The bucket gives you a stable public input endpoint. Start forwarding to your local server: ``` relay forward --bucket asana http://localhost:8080/webhook ``` The agent opens an **outbound** connection to Webhook Relay and streams every incoming request down to `http://localhost:8080/webhook`. Because the connection is outbound, there are **no firewall ports to open and no public IP needed** — this works from your laptop, behind a corporate proxy, or inside a Kubernetes cluster. Running in Docker? The same command works in the official `webhookrelay/webhookrelayd` image. Full details are in the [localhost forwarding docs](https://webhookrelay.com/blog/receive-asana-webhooks-locally/docs/webhooks/internal/localhost/). Now point the Asana webhook at your Webhook Relay endpoint (or create it there from the start), trigger an event, and watch it arrive on `localhost`. ## [Asana-specific configuration and quirks](#asana-specific-configuration-and-quirks) A few Asana details worth knowing: - **Programmatic only:** create webhooks via `POST /webhooks` in the Asana API, not a settings page. - **Handshake:** echo the `X-Hook-Secret` header on the first request or the webhook won't activate. A bin shows you the exact value. - **Payload:** an `events` array of changes — you call the API to fetch full resource details. ## [Step 3: Verify the Asana webhook signature](#step-3-verify-the-asana-webhook-signature) Asana's setup has two parts. On creation it sends a handshake with an **`X-Hook-Secret`** header that you must **echo back** in your response (status 200) — save that secret. Afterwards, every delivery is signed: Asana computes an **HMAC-SHA256** (hex) of the **raw body** with that secret and sends it in the **`X-Hook-Signature`** header. Recompute and compare in constant time. To sanity-check an HMAC implementation, paste a captured body, your secret, and the received signature into the free [HMAC signature verifier](https://webhookrelay.com/blog/receive-asana-webhooks-locally/hmac-verification/). For language-specific code and the common pitfalls (reading the body _after_ a JSON parser has already consumed it, timing-safe comparison), read [Verify a webhook signature](https://webhookrelay.com/blog/receive-asana-webhooks-locally/blog/verify-webhook-signature/). ## [Replay and iterate](#replay-and-iterate) This is where local development gets fast: - **Replay from Webhook Relay** — past requests are stored on your bucket, so you can resend a captured event against your handler without touching Asana at all. - **Iterate on your handler** by editing code and replaying the same delivery until it behaves correctly. No commits, no pushes, no deploys just to test a single code path. - **Keep `relay forward` running** while you work — events stream straight to `localhost` as you trigger them in Asana. Because the Webhook Relay endpoint is stable, you can stop and restart the agent, reboot your machine, or come back next week — the Asana configuration never needs to change. ## [Get started](#get-started) 1. Inspect the real payload in the free [Webhook Bin](https://webhookrelay.com/blog/receive-asana-webhooks-locally/webhook-bin/) — no signup needed. 2. [Create a Webhook Relay account](https://my.webhookrelay.com/register), install the agent, and run `relay forward --bucket asana http://localhost:8080/webhook`. 3. Point your Asana webhook at the stable endpoint, trigger an event, and watch it hit `localhost`. You will be testing real Asana events against your local handler in a few minutes — no deploys, no open firewall ports, and a URL you configure exactly once. --- --- title: Test Box Webhooks Locally | WebhookRelay meta: "og: title": "Test Box Webhooks Locally" description: Test Box webhooks locally and receive them on localhost without deploying. Inspect the real payload, forward to your handler, and verify the signature. url: https://webhookrelay.com/blog/receive-box-webhooks-locally.md file: /blog/receive-box-webhooks-locally.md --- ![Stripes](https://webhookrelay.com/blog/receive-box-webhooks-locally/images/stripes.svg) # **Test Box Webhooks Locally** Test Box webhooks locally and receive them on localhost without deploying. Inspect the real payload, forward to your handler, and verify the signature. ![Test Box Webhooks Locally (Receive Box Webhooks on localhost)](https://webhookrelay.com/blog/receive-box-webhooks-locally/images/blog/heroes/localhost.jpg) You are building a Box integration and you need to watch your handler react to a real event. The problem hits immediately: Box will only POST to a **public URL**, and your handler is running on `localhost:8080`. Box has no way to reach it. The usual workarounds are slow. Deploying to a staging environment for every code change kills your iteration speed. Copying a sample payload out of the docs into curl gives you a _guess_ at the real request, not the exact headers and body Box actually sends. What you really want is to **test Box webhooks locally** — real events, hitting your local handler, on a URL that does not change every time you restart. This guide shows how to do exactly that. ## [Why testing Box webhooks locally is tricky](#why-testing-box-webhooks-locally-is-tricky) A webhook is just an HTTP request that Box sends to a URL when something changes. Box lives on the public internet; your dev machine usually does not. It sits behind a router, a corporate firewall, or both, with no public IP and no inbound ports open. So you need something in the middle: a public endpoint Box can hit that relays each request down to your laptop without you opening a single firewall port. That is what [Webhook Relay](https://my.webhookrelay.com/register) does — and unlike a random tunnel URL, the endpoint is **stable**, so you configure Box once and never touch it again. ## [Step 1: Inspect the real payload with Webhook Bin](#step-1-inspect-the-real-payload-with-webhook-bin) Before you write any handler code, find out what Box actually sends. Open the free [Webhook Bin](https://webhookrelay.com/blog/receive-box-webhooks-locally/webhook-bin/) — no signup — and you get an instant public URL. 1. Copy the Webhook Bin URL. 2. In the Box Developer Console, create a **Webhook (V2)** on a file or folder, set the URL, choose the triggers, and copy the **primary** and **secondary** signature keys. 3. Trigger a real event and inspect the captured request. You will see the full body and every header. The body is JSON describing the trigger and the source item. Each request carries `Box-Signature-Primary`, `Box-Signature-Secondary` and `Box-Delivery-Timestamp` headers. Now you know the exact shape of the data before writing a line of code. For more on this approach, see [How to test webhooks](https://webhookrelay.com/blog/receive-box-webhooks-locally/blog/how-to-test-webhooks/) and [What is a webhook](https://webhookrelay.com/blog/receive-box-webhooks-locally/blog/what-is-webhook/). ## [Step 2: Forward the events to localhost with the relay agent](#step-2-forward-the-events-to-localhost-with-the-relay-agent) Once you know the payload, route those same events into your local handler. [Sign up for Webhook Relay](https://my.webhookrelay.com/register), install the `relay` agent (CLI or Docker), and create a bucket — say `box`. The bucket gives you a stable public input endpoint. Start forwarding to your local server: ``` relay forward --bucket box http://localhost:8080/webhook ``` The agent opens an **outbound** connection to Webhook Relay and streams every incoming request down to `http://localhost:8080/webhook`. Because the connection is outbound, there are **no firewall ports to open and no public IP needed** — this works from your laptop, behind a corporate proxy, or inside a Kubernetes cluster. Running in Docker? The same command works in the official `webhookrelay/webhookrelayd` image. Full details are in the [localhost forwarding docs](https://webhookrelay.com/blog/receive-box-webhooks-locally/docs/webhooks/internal/localhost/). Now point the Box webhook at your Webhook Relay endpoint (or create it there from the start), trigger an event, and watch it arrive on `localhost`. ## [Box-specific configuration and quirks](#box-specific-configuration-and-quirks) A few Box details worth knowing: - **Where to add it:** the **Box Developer Console** → your app → Webhooks (V2) on a file/folder. - **Two keys:** primary + secondary let you rotate without downtime — accept either. - **Timestamp:** the signature covers `body + Box-Delivery-Timestamp`; reject stale ones. ## [Step 3: Verify the Box webhook signature](#step-3-verify-the-box-webhook-signature) Box signs each request with **HMAC-SHA256** (base64) over the **raw body concatenated with the `Box-Delivery-Timestamp`**, using your **primary** signature key, and sends it in **`Box-Signature-Primary`** (with `Box-Signature-Secondary` for key rotation). Validate against either key, and reject deliveries whose `Box-Delivery-Timestamp` is older than ~10 minutes. To sanity-check an HMAC implementation, paste a captured body, your secret, and the received signature into the free [HMAC signature verifier](https://webhookrelay.com/blog/receive-box-webhooks-locally/hmac-verification/). For language-specific code and the common pitfalls (reading the body _after_ a JSON parser has already consumed it, timing-safe comparison), read [Verify a webhook signature](https://webhookrelay.com/blog/receive-box-webhooks-locally/blog/verify-webhook-signature/). ## [Replay and iterate](#replay-and-iterate) This is where local development gets fast: - **Replay from Webhook Relay** — past requests are stored on your bucket, so you can resend a captured event against your handler without touching Box at all. - **Iterate on your handler** by editing code and replaying the same delivery until it behaves correctly. No commits, no pushes, no deploys just to test a single code path. - **Keep `relay forward` running** while you work — events stream straight to `localhost` as you trigger them in Box. Because the Webhook Relay endpoint is stable, you can stop and restart the agent, reboot your machine, or come back next week — the Box configuration never needs to change. ## [Get started](#get-started) 1. Inspect the real payload in the free [Webhook Bin](https://webhookrelay.com/blog/receive-box-webhooks-locally/webhook-bin/) — no signup needed. 2. [Create a Webhook Relay account](https://my.webhookrelay.com/register), install the agent, and run `relay forward --bucket box http://localhost:8080/webhook`. 3. Point your Box webhook at the stable endpoint, trigger an event, and watch it hit `localhost`. You will be testing real Box events against your local handler in a few minutes — no deploys, no open firewall ports, and a URL you configure exactly once. --- --- title: Test Okta Webhooks Locally | WebhookRelay meta: "og: title": "Test Okta Webhooks Locally" description: Test Okta webhooks locally and receive them on localhost without deploying. Inspect the real payload, forward to your handler, and verify the signature. url: https://webhookrelay.com/blog/receive-okta-webhooks-locally.md file: /blog/receive-okta-webhooks-locally.md --- ![Stripes](https://webhookrelay.com/blog/receive-okta-webhooks-locally/images/stripes.svg) # **Test Okta Webhooks Locally** Test Okta webhooks locally and receive them on localhost without deploying. Inspect the real payload, forward to your handler, and verify the signature. ![Test Okta Webhooks Locally (Receive Okta Webhooks on localhost)](https://webhookrelay.com/blog/receive-okta-webhooks-locally/images/blog/heroes/localhost.jpg) You are building a Okta integration and you need to watch your handler react to a real event. The problem hits immediately: Okta will only POST to a **public URL**, and your handler is running on `localhost:8080`. Okta has no way to reach it. The usual workarounds are slow. Deploying to a staging environment for every code change kills your iteration speed. Copying a sample payload out of the docs into curl gives you a _guess_ at the real request, not the exact headers and body Okta actually sends. What you really want is to **test Okta webhooks locally** — real events, hitting your local handler, on a URL that does not change every time you restart. This guide shows how to do exactly that. ## [Why testing Okta webhooks locally is tricky](#why-testing-okta-webhooks-locally-is-tricky) A webhook is just an HTTP request that Okta sends to a URL when something changes. Okta lives on the public internet; your dev machine usually does not. It sits behind a router, a corporate firewall, or both, with no public IP and no inbound ports open. So you need something in the middle: a public endpoint Okta can hit that relays each request down to your laptop without you opening a single firewall port. That is what [Webhook Relay](https://my.webhookrelay.com/register) does — and unlike a random tunnel URL, the endpoint is **stable**, so you configure Okta once and never touch it again. ## [Step 1: Inspect the real payload with Webhook Bin](#step-1-inspect-the-real-payload-with-webhook-bin) Before you write any handler code, find out what Okta actually sends. Open the free [Webhook Bin](https://webhookrelay.com/blog/receive-okta-webhooks-locally/webhook-bin/) — no signup — and you get an instant public URL. 1. Copy the Webhook Bin URL. 2. In the Okta Admin Console, go to **Workflow → Event Hooks → Create Event Hook**, set the URL, choose the events, and add an **authentication header** secret. Okta then sends a one-time verification request. 3. Trigger a real event and inspect the captured request. You will see the full body and every header. Okta first sends a **one-time verification GET** with an `X-Okta-Verification-Challenge` header — respond with `{ "verification": "" }`. Event deliveries are JSON with an `eventType` and a `data.events` array of System Log events. Now you know the exact shape of the data before writing a line of code. For more on this approach, see [How to test webhooks](https://webhookrelay.com/blog/receive-okta-webhooks-locally/blog/how-to-test-webhooks/) and [What is a webhook](https://webhookrelay.com/blog/receive-okta-webhooks-locally/blog/what-is-webhook/). ## [Step 2: Forward the events to localhost with the relay agent](#step-2-forward-the-events-to-localhost-with-the-relay-agent) Once you know the payload, route those same events into your local handler. [Sign up for Webhook Relay](https://my.webhookrelay.com/register), install the `relay` agent (CLI or Docker), and create a bucket — say `okta`. The bucket gives you a stable public input endpoint. Start forwarding to your local server: ``` relay forward --bucket okta http://localhost:8080/webhook ``` The agent opens an **outbound** connection to Webhook Relay and streams every incoming request down to `http://localhost:8080/webhook`. Because the connection is outbound, there are **no firewall ports to open and no public IP needed** — this works from your laptop, behind a corporate proxy, or inside a Kubernetes cluster. Running in Docker? The same command works in the official `webhookrelay/webhookrelayd` image. Full details are in the [localhost forwarding docs](https://webhookrelay.com/blog/receive-okta-webhooks-locally/docs/webhooks/internal/localhost/). Now point the Okta webhook at your Webhook Relay endpoint (or create it there from the start), trigger an event, and watch it arrive on `localhost`. ## [Okta-specific configuration and quirks](#okta-specific-configuration-and-quirks) A few Okta details worth knowing: - **Verification GET:** echo the `X-Okta-Verification-Challenge` as `{"verification": "..."}` or the hook won't verify. - **No HMAC:** secure it with the custom **authentication header** secret you configure. - **Payload:** `eventType` + a `data.events` array of System Log events. ## [Step 3: Verify the Okta webhook signature](#step-3-verify-the-okta-webhook-signature) Okta Event Hooks don't sign the body with an HMAC. Security is two-part: (1) a **one-time verification GET** carrying an `X-Okta-Verification-Challenge` header, which you answer by returning `{ "verification": "" }`; and (2) a custom **authentication header** (for example `Authorization: `) that you set when creating the hook and Okta sends on every event — check it matches before trusting the payload. To sanity-check an HMAC implementation, paste a captured body, your secret, and the received signature into the free [HMAC signature verifier](https://webhookrelay.com/blog/receive-okta-webhooks-locally/hmac-verification/). For language-specific code and the common pitfalls (reading the body _after_ a JSON parser has already consumed it, timing-safe comparison), read [Verify a webhook signature](https://webhookrelay.com/blog/receive-okta-webhooks-locally/blog/verify-webhook-signature/). ## [Replay and iterate](#replay-and-iterate) This is where local development gets fast: - **Replay from Webhook Relay** — past requests are stored on your bucket, so you can resend a captured event against your handler without touching Okta at all. - **Iterate on your handler** by editing code and replaying the same delivery until it behaves correctly. No commits, no pushes, no deploys just to test a single code path. - **Keep `relay forward` running** while you work — events stream straight to `localhost` as you trigger them in Okta. Because the Webhook Relay endpoint is stable, you can stop and restart the agent, reboot your machine, or come back next week — the Okta configuration never needs to change. ## [Get started](#get-started) 1. Inspect the real payload in the free [Webhook Bin](https://webhookrelay.com/blog/receive-okta-webhooks-locally/webhook-bin/) — no signup needed. 2. [Create a Webhook Relay account](https://my.webhookrelay.com/register), install the agent, and run `relay forward --bucket okta http://localhost:8080/webhook`. 3. Point your Okta webhook at the stable endpoint, trigger an event, and watch it hit `localhost`. You will be testing real Okta events against your local handler in a few minutes — no deploys, no open firewall ports, and a URL you configure exactly once. --- --- title: Test Dropbox Webhooks Locally | WebhookRelay meta: "og: title": "Test Dropbox Webhooks Locally" description: Test Dropbox webhooks locally and receive them on localhost without deploying. Inspect the real payload, forward to your handler, and verify the signature. url: https://webhookrelay.com/blog/receive-dropbox-webhooks-locally.md file: /blog/receive-dropbox-webhooks-locally.md --- ![Stripes](https://webhookrelay.com/blog/receive-dropbox-webhooks-locally/images/stripes.svg) # **Test Dropbox Webhooks Locally** Test Dropbox webhooks locally and receive them on localhost without deploying. Inspect the real payload, forward to your handler, and verify the signature. ![Test Dropbox Webhooks Locally (Receive Dropbox Webhooks on localhost)](https://webhookrelay.com/blog/receive-dropbox-webhooks-locally/images/blog/heroes/localhost.jpg) You are building a Dropbox integration and you need to watch your handler react to a real event. The problem hits immediately: Dropbox will only POST to a **public URL**, and your handler is running on `localhost:8080`. Dropbox has no way to reach it. The usual workarounds are slow. Deploying to a staging environment for every code change kills your iteration speed. Copying a sample payload out of the docs into curl gives you a _guess_ at the real request, not the exact headers and body Dropbox actually sends. What you really want is to **test Dropbox webhooks locally** — real events, hitting your local handler, on a URL that does not change every time you restart. This guide shows how to do exactly that. ## [Why testing Dropbox webhooks locally is tricky](#why-testing-dropbox-webhooks-locally-is-tricky) A webhook is just an HTTP request that Dropbox sends to a URL when something changes. Dropbox lives on the public internet; your dev machine usually does not. It sits behind a router, a corporate firewall, or both, with no public IP and no inbound ports open. So you need something in the middle: a public endpoint Dropbox can hit that relays each request down to your laptop without you opening a single firewall port. That is what [Webhook Relay](https://my.webhookrelay.com/register) does — and unlike a random tunnel URL, the endpoint is **stable**, so you configure Dropbox once and never touch it again. ## [Step 1: Inspect the real payload with Webhook Bin](#step-1-inspect-the-real-payload-with-webhook-bin) Before you write any handler code, find out what Dropbox actually sends. Open the free [Webhook Bin](https://webhookrelay.com/blog/receive-dropbox-webhooks-locally/webhook-bin/) — no signup — and you get an instant public URL. 1. Copy the Webhook Bin URL. 2. In the Dropbox App Console, add your URL under **Webhooks**. Dropbox first sends a **verification GET** with a `challenge` query parameter that you must echo back. 3. Trigger a real event and inspect the captured request. You will see the full body and every header. Dropbox verification is a **GET** with a `challenge` query parameter — respond with the challenge value (and `X-Content-Type-Options: nosniff`). Change notifications then arrive as `POST`s that simply list the accounts with changes; you call the API to fetch the actual deltas. Now you know the exact shape of the data before writing a line of code. For more on this approach, see [How to test webhooks](https://webhookrelay.com/blog/receive-dropbox-webhooks-locally/blog/how-to-test-webhooks/) and [What is a webhook](https://webhookrelay.com/blog/receive-dropbox-webhooks-locally/blog/what-is-webhook/). ## [Step 2: Forward the events to localhost with the relay agent](#step-2-forward-the-events-to-localhost-with-the-relay-agent) Once you know the payload, route those same events into your local handler. [Sign up for Webhook Relay](https://my.webhookrelay.com/register), install the `relay` agent (CLI or Docker), and create a bucket — say `dropbox`. The bucket gives you a stable public input endpoint. Start forwarding to your local server: ``` relay forward --bucket dropbox http://localhost:8080/webhook ``` The agent opens an **outbound** connection to Webhook Relay and streams every incoming request down to `http://localhost:8080/webhook`. Because the connection is outbound, there are **no firewall ports to open and no public IP needed** — this works from your laptop, behind a corporate proxy, or inside a Kubernetes cluster. Running in Docker? The same command works in the official `webhookrelay/webhookrelayd` image. Full details are in the [localhost forwarding docs](https://webhookrelay.com/blog/receive-dropbox-webhooks-locally/docs/webhooks/internal/localhost/). Now point the Dropbox webhook at your Webhook Relay endpoint (or create it there from the start), trigger an event, and watch it arrive on `localhost`. ## [Dropbox-specific configuration and quirks](#dropbox-specific-configuration-and-quirks) A few Dropbox details worth knowing: - **Verification GET:** echo the `challenge` query param or the webhook won't enable. - **Thin notifications:** a POST only tells you _which accounts_ changed — call the API for the deltas. - **Signature:** `X-Dropbox-Signature` is HMAC-SHA256 of the raw body with your app secret. ## [Step 3: Verify the Dropbox webhook signature](#step-3-verify-the-dropbox-webhook-signature) Two steps. First the **verification GET**: echo back the `challenge` query parameter as plain text. Then each notification **POST** is signed — Dropbox computes an **HMAC-SHA256** (hex) of the **raw body** with your **app secret** and sends it in the **`X-Dropbox-Signature`** header. Recompute over the raw body and compare in constant time. To sanity-check an HMAC implementation, paste a captured body, your secret, and the received signature into the free [HMAC signature verifier](https://webhookrelay.com/blog/receive-dropbox-webhooks-locally/hmac-verification/). For language-specific code and the common pitfalls (reading the body _after_ a JSON parser has already consumed it, timing-safe comparison), read [Verify a webhook signature](https://webhookrelay.com/blog/receive-dropbox-webhooks-locally/blog/verify-webhook-signature/). ## [Replay and iterate](#replay-and-iterate) This is where local development gets fast: - **Replay from Webhook Relay** — past requests are stored on your bucket, so you can resend a captured event against your handler without touching Dropbox at all. - **Iterate on your handler** by editing code and replaying the same delivery until it behaves correctly. No commits, no pushes, no deploys just to test a single code path. - **Keep `relay forward` running** while you work — events stream straight to `localhost` as you trigger them in Dropbox. Because the Webhook Relay endpoint is stable, you can stop and restart the agent, reboot your machine, or come back next week — the Dropbox configuration never needs to change. ## [Get started](#get-started) 1. Inspect the real payload in the free [Webhook Bin](https://webhookrelay.com/blog/receive-dropbox-webhooks-locally/webhook-bin/) — no signup needed. 2. [Create a Webhook Relay account](https://my.webhookrelay.com/register), install the agent, and run `relay forward --bucket dropbox http://localhost:8080/webhook`. 3. Point your Dropbox webhook at the stable endpoint, trigger an event, and watch it hit `localhost`. You will be testing real Dropbox events against your local handler in a few minutes — no deploys, no open firewall ports, and a URL you configure exactly once. --- --- title: Stripe Webhook Tester — Test & Inspect Online | WebhookRelay meta: "og: title": "Stripe Webhook Tester — Test & Inspect Online" description: Test and inspect Stripe webhooks online with a free webhook tester URL — capture real Stripe payloads, read the signature header, then forward locally. url: https://webhookrelay.com/blog/stripe-webhook-tester.md file: /blog/stripe-webhook-tester.md --- ![Stripes](https://webhookrelay.com/blog/stripe-webhook-tester/images/stripes.svg) # **Stripe Webhook Tester — Test & Inspect Online** Test and inspect Stripe webhooks online with a free webhook tester URL — capture real Stripe payloads, read the signature header, then forward locally. ![Stripe Webhook Tester — Test & Inspect Stripe Webhooks Online](https://webhookrelay.com/blog/stripe-webhook-tester/images/blog/heroes/tester.jpg) If you are wiring up Stripe webhooks, the first question is always the same: _what does Stripe actually send?_ The docs show an idealised payload, but the real request — its headers, its `Stripe-Signature` header, the exact JSON shape — is what your handler has to parse. A **Stripe webhook tester** gives you a public URL that captures those real requests so you can read every byte before you write any code. ## [Get a free Stripe webhook tester URL](#get-a-free-stripe-webhook-tester-url) The fastest way is our free [Webhook Bin](https://webhookrelay.com/blog/stripe-webhook-tester/webhook-bin/) — a no-code [webhook tester](https://webhookrelay.com/blog/stripe-webhook-tester/webhook-bin/) that gives you an instant public URL and stores every request that hits it, headers and body included. No signup, no deploy: 1. Open the [Webhook Bin](https://webhookrelay.com/blog/stripe-webhook-tester/webhook-bin/) and copy the URL it generates for you. 2. In **Developers → Webhooks in the Stripe Dashboard**, add a webhook endpoint and paste that URL. 3. Trigger an event (see below) and watch the request land in the bin in real time. Because the bin keeps the full request, you can inspect the `Stripe-Signature` header, the `Content-Type`, and the complete payload — the three things you need to build and verify a handler. ## [What a Stripe webhook looks like](#what-a-stripe-webhook-looks-like) Stripe delivers webhooks as an HTTP POST with a `application/json` body. Stripe lets you fire test events straight from the dashboard or with the Stripe CLI, so you can replay a real `payment_intent.succeeded` into your bin and read every field it actually sends. A typical `payment_intent.succeeded` payload looks like this: ``` { "id": "evt_1P...", "object": "event", "type": "payment_intent.succeeded", "data": { "object": { "id": "pi_3P...", "object": "payment_intent", "amount": 1999, "currency": "usd", "status": "succeeded" } } } ``` Common Stripe events you will want to test: - `payment_intent.succeeded` - `checkout.session.completed` - `customer.subscription.updated` - `invoice.paid` ## [Verifying the Stripe signature](#verifying-the-stripe-signature) Stripe signs each request so you can prove it really came from Stripe. The signature travels in the **`Stripe-Signature`** header and is HMAC-SHA256 computed over `timestamp.payload`, using a signing secret that starts with `whsec_`. Capture a real request first, then use our [Stripe signature verifier](https://webhookrelay.com/blog/stripe-webhook-tester/verify-stripe-webhook-signature/) and the [verify a webhook signature](https://webhookrelay.com/blog/stripe-webhook-tester/blog/verify-webhook-signature/) guide to confirm your verification logic against a payload you can actually see. ## [From inspecting to receiving on localhost](#from-inspecting-to-receiving-on-localhost) A bin is perfect for _seeing_ the payload. When you are ready to drive your **local** handler with real Stripe events — without deploying — forward them straight to `localhost` with the Webhook Relay agent. The full walkthrough is here: [Receive Stripe webhooks on localhost](https://webhookrelay.com/blog/stripe-webhook-tester/blog/receiving-stripe-webhooks-localhost/). That gives you a stable public URL that tunnels to your machine, so Stripe keeps delivering to the same endpoint while you iterate on `localhost`, no firewall changes or public IP required. ## [Test Stripe webhooks online in three steps](#test-stripe-webhooks-online-in-three-steps) 1. **Capture** — point Stripe at a [Webhook Bin](https://webhookrelay.com/blog/stripe-webhook-tester/webhook-bin/) URL and inspect the real request. 2. **Verify** — confirm the `Stripe-Signature` header with the [Stripe signature verifier](https://webhookrelay.com/blog/stripe-webhook-tester/verify-stripe-webhook-signature/). 3. **Forward** — when the shape is clear, [receive Stripe webhooks on localhost](https://webhookrelay.com/blog/stripe-webhook-tester/blog/receiving-stripe-webhooks-localhost/) and build your handler. New to webhooks in general? Start with [what is a webhook](https://webhookrelay.com/blog/stripe-webhook-tester/blog/what-is-webhook/) and [how to test webhooks](https://webhookrelay.com/blog/stripe-webhook-tester/blog/how-to-test-webhooks/). Ready to inspect your first Stripe event? [Open a free Webhook Bin](https://webhookrelay.com/blog/stripe-webhook-tester/webhook-bin/) and paste the URL into Stripe. --- --- title: Test Webflow Webhooks Locally | WebhookRelay meta: "og: title": "Test Webflow Webhooks Locally" description: Test Webflow webhooks locally and receive them on localhost without deploying. Inspect the real payload, forward to your handler, and verify the signature. url: https://webhookrelay.com/blog/receive-webflow-webhooks-locally.md file: /blog/receive-webflow-webhooks-locally.md --- ![Stripes](https://webhookrelay.com/blog/receive-webflow-webhooks-locally/images/stripes.svg) # **Test Webflow Webhooks Locally** Test Webflow webhooks locally and receive them on localhost without deploying. Inspect the real payload, forward to your handler, and verify the signature. ![Test Webflow Webhooks Locally (Receive Webflow Webhooks on localhost)](https://webhookrelay.com/blog/receive-webflow-webhooks-locally/images/blog/heroes/localhost.jpg) You are building a Webflow integration and you need to watch your handler react to a real event. The problem hits immediately: Webflow will only POST to a **public URL**, and your handler is running on `localhost:8080`. Webflow has no way to reach it. The usual workarounds are slow. Deploying to a staging environment for every code change kills your iteration speed. Copying a sample payload out of the docs into curl gives you a _guess_ at the real request, not the exact headers and body Webflow actually sends. What you really want is to **test Webflow webhooks locally** — real events, hitting your local handler, on a URL that does not change every time you restart. This guide shows how to do exactly that. ## [Why testing Webflow webhooks locally is tricky](#why-testing-webflow-webhooks-locally-is-tricky) A webhook is just an HTTP request that Webflow sends to a URL when something changes. Webflow lives on the public internet; your dev machine usually does not. It sits behind a router, a corporate firewall, or both, with no public IP and no inbound ports open. So you need something in the middle: a public endpoint Webflow can hit that relays each request down to your laptop without you opening a single firewall port. That is what [Webhook Relay](https://my.webhookrelay.com/register) does — and unlike a random tunnel URL, the endpoint is **stable**, so you configure Webflow once and never touch it again. ## [Step 1: Inspect the real payload with Webhook Bin](#step-1-inspect-the-real-payload-with-webhook-bin) Before you write any handler code, find out what Webflow actually sends. Open the free [Webhook Bin](https://webhookrelay.com/blog/receive-webflow-webhooks-locally/webhook-bin/) — no signup — and you get an instant public URL. 1. Copy the Webhook Bin URL. 2. Add a webhook in the Webflow site settings under **Apps & integrations → Webhooks** (or via the Data API), set the URL, and choose the trigger. 3. Trigger a real event and inspect the captured request. You will see the full body and every header. The body is JSON with a `triggerType` and the relevant payload — form data, a collection item, a publish event. Data API v2 webhooks include `x-webflow-signature` and `x-webflow-timestamp` headers. Now you know the exact shape of the data before writing a line of code. For more on this approach, see [How to test webhooks](https://webhookrelay.com/blog/receive-webflow-webhooks-locally/blog/how-to-test-webhooks/) and [What is a webhook](https://webhookrelay.com/blog/receive-webflow-webhooks-locally/blog/what-is-webhook/). ## [Step 2: Forward the events to localhost with the relay agent](#step-2-forward-the-events-to-localhost-with-the-relay-agent) Once you know the payload, route those same events into your local handler. [Sign up for Webhook Relay](https://my.webhookrelay.com/register), install the `relay` agent (CLI or Docker), and create a bucket — say `webflow`. The bucket gives you a stable public input endpoint. Start forwarding to your local server: ``` relay forward --bucket webflow http://localhost:8080/webhook ``` The agent opens an **outbound** connection to Webhook Relay and streams every incoming request down to `http://localhost:8080/webhook`. Because the connection is outbound, there are **no firewall ports to open and no public IP needed** — this works from your laptop, behind a corporate proxy, or inside a Kubernetes cluster. Running in Docker? The same command works in the official `webhookrelay/webhookrelayd` image. Full details are in the [localhost forwarding docs](https://webhookrelay.com/blog/receive-webflow-webhooks-locally/docs/webhooks/internal/localhost/). Now point the Webflow webhook at your Webhook Relay endpoint (or create it there from the start), trigger an event, and watch it arrive on `localhost`. ## [Webflow-specific configuration and quirks](#webflow-specific-configuration-and-quirks) A few Webflow details worth knowing: - **Where to add it:** **Site settings → Apps & integrations → Webhooks**, or the Data API. - **Triggers:** `form_submission`, `site_publish`, `collection_item_created` and more. - **Signature:** v2 signs `timestamp:body`; older legacy webhooks aren't signed. ## [Step 3: Verify the Webflow webhook signature](#step-3-verify-the-webflow-webhook-signature) Webflow's Data API v2 signs `"{x-webflow-timestamp}:{raw body}"` with **HMAC-SHA256** using your app's **client secret** and sends the hex digest in the **`x-webflow-signature`** header. Rebuild `timestamp + ":" + rawBody`, compute the HMAC, and compare; reject requests whose `x-webflow-timestamp` is older than ~5 minutes. To sanity-check an HMAC implementation, paste a captured body, your secret, and the received signature into the free [HMAC signature verifier](https://webhookrelay.com/blog/receive-webflow-webhooks-locally/hmac-verification/). For language-specific code and the common pitfalls (reading the body _after_ a JSON parser has already consumed it, timing-safe comparison), read [Verify a webhook signature](https://webhookrelay.com/blog/receive-webflow-webhooks-locally/blog/verify-webhook-signature/). ## [Replay and iterate](#replay-and-iterate) This is where local development gets fast: - **Replay from Webhook Relay** — past requests are stored on your bucket, so you can resend a captured event against your handler without touching Webflow at all. - **Iterate on your handler** by editing code and replaying the same delivery until it behaves correctly. No commits, no pushes, no deploys just to test a single code path. - **Keep `relay forward` running** while you work — events stream straight to `localhost` as you trigger them in Webflow. Because the Webhook Relay endpoint is stable, you can stop and restart the agent, reboot your machine, or come back next week — the Webflow configuration never needs to change. ## [Get started](#get-started) 1. Inspect the real payload in the free [Webhook Bin](https://webhookrelay.com/blog/receive-webflow-webhooks-locally/webhook-bin/) — no signup needed. 2. [Create a Webhook Relay account](https://my.webhookrelay.com/register), install the agent, and run `relay forward --bucket webflow http://localhost:8080/webhook`. 3. Point your Webflow webhook at the stable endpoint, trigger an event, and watch it hit `localhost`. You will be testing real Webflow events against your local handler in a few minutes — no deploys, no open firewall ports, and a URL you configure exactly once. --- --- title: GitHub Webhook Tester — Test & Inspect Online | WebhookRelay meta: "og: title": "GitHub Webhook Tester — Test & Inspect Online" description: Test and inspect GitHub webhooks online with a free webhook tester URL — capture real GitHub payloads, read the signature header, then forward locally. url: https://webhookrelay.com/blog/github-webhook-tester.md file: /blog/github-webhook-tester.md --- ![Stripes](https://webhookrelay.com/blog/github-webhook-tester/images/stripes.svg) # **GitHub Webhook Tester — Test & Inspect Online** Test and inspect GitHub webhooks online with a free webhook tester URL — capture real GitHub payloads, read the signature header, then forward locally. ![GitHub Webhook Tester — Test & Inspect GitHub Webhooks Online](https://webhookrelay.com/blog/github-webhook-tester/images/blog/heroes/tester.jpg) If you are wiring up GitHub webhooks, the first question is always the same: _what does GitHub actually send?_ The docs show an idealised payload, but the real request — its headers, its `X-Hub-Signature-256` header, the exact JSON shape — is what your handler has to parse. A **GitHub webhook tester** gives you a public URL that captures those real requests so you can read every byte before you write any code. ## [Get a free GitHub webhook tester URL](#get-a-free-github-webhook-tester-url) The fastest way is our free [Webhook Bin](https://webhookrelay.com/blog/github-webhook-tester/webhook-bin/) — a no-code [webhook tester](https://webhookrelay.com/blog/github-webhook-tester/webhook-bin/) that gives you an instant public URL and stores every request that hits it, headers and body included. No signup, no deploy: 1. Open the [Webhook Bin](https://webhookrelay.com/blog/github-webhook-tester/webhook-bin/) and copy the URL it generates for you. 2. In **Settings → Webhooks on a repository or organization**, add a webhook endpoint and paste that URL. 3. Trigger an event (see below) and watch the request land in the bin in real time. Because the bin keeps the full request, you can inspect the `X-Hub-Signature-256` header, the `Content-Type`, and the complete payload — the three things you need to build and verify a handler. ## [What a GitHub webhook looks like](#what-a-github-webhook-looks-like) GitHub delivers webhooks as an HTTP POST with a `application/json (or`application/x-www-form-urlencoded`)` body. GitHub adds an `X-GitHub-Event` header naming the event and an `X-GitHub-Delivery` GUID for each delivery, and its Recent Deliveries panel lets you redeliver any payload — perfect for replaying into a tester URL. A typical `push` payload looks like this: ``` { "action": "opened", "number": 42, "pull_request": { "id": 1, "title": "Add feature", "state": "open", "user": { "login": "octocat" } }, "repository": { "full_name": "octocat/hello-world" } } ``` Common GitHub events you will want to test: - `push` - `pull_request` - `issues` - `release` ## [Verifying the GitHub signature](#verifying-the-github-signature) GitHub signs each request so you can prove it really came from GitHub. The signature travels in the **`X-Hub-Signature-256`** header and is HMAC-SHA256 of the raw request body, using the secret you set on the webhook. Capture a real request first, then use our [GitHub signature verifier](https://webhookrelay.com/blog/github-webhook-tester/verify-github-webhook-signature/) and the [verify a webhook signature](https://webhookrelay.com/blog/github-webhook-tester/blog/verify-webhook-signature/) guide to confirm your verification logic against a payload you can actually see. ## [From inspecting to receiving on localhost](#from-inspecting-to-receiving-on-localhost) A bin is perfect for _seeing_ the payload. When you are ready to drive your **local** handler with real GitHub events — without deploying — forward them straight to `localhost` with the Webhook Relay agent. The full walkthrough is here: [Receive GitHub webhooks on localhost](https://webhookrelay.com/blog/github-webhook-tester/blog/receive-github-webhooks-locally/). That gives you a stable public URL that tunnels to your machine, so GitHub keeps delivering to the same endpoint while you iterate on `localhost`, no firewall changes or public IP required. ## [Test GitHub webhooks online in three steps](#test-github-webhooks-online-in-three-steps) 1. **Capture** — point GitHub at a [Webhook Bin](https://webhookrelay.com/blog/github-webhook-tester/webhook-bin/) URL and inspect the real request. 2. **Verify** — confirm the `X-Hub-Signature-256` header with the [GitHub signature verifier](https://webhookrelay.com/blog/github-webhook-tester/verify-github-webhook-signature/). 3. **Forward** — when the shape is clear, [receive GitHub webhooks on localhost](https://webhookrelay.com/blog/github-webhook-tester/blog/receive-github-webhooks-locally/) and build your handler. New to webhooks in general? Start with [what is a webhook](https://webhookrelay.com/blog/github-webhook-tester/blog/what-is-webhook/) and [how to test webhooks](https://webhookrelay.com/blog/github-webhook-tester/blog/how-to-test-webhooks/). Ready to inspect your first GitHub event? [Open a free Webhook Bin](https://webhookrelay.com/blog/github-webhook-tester/webhook-bin/) and paste the URL into GitHub. --- --- title: Twilio Webhook Tester — Test & Inspect Online | WebhookRelay meta: "og: title": "Twilio Webhook Tester — Test & Inspect Online" description: Test and inspect Twilio webhooks online with a free webhook tester URL — capture real Twilio payloads, read the signature header, then forward locally. url: https://webhookrelay.com/blog/twilio-webhook-tester.md file: /blog/twilio-webhook-tester.md --- ![Stripes](https://webhookrelay.com/blog/twilio-webhook-tester/images/stripes.svg) # **Twilio Webhook Tester — Test & Inspect Online** Test and inspect Twilio webhooks online with a free webhook tester URL — capture real Twilio payloads, read the signature header, then forward locally. ![Twilio Webhook Tester — Test & Inspect Twilio Webhooks Online](https://webhookrelay.com/blog/twilio-webhook-tester/images/blog/heroes/tester.jpg) If you are wiring up Twilio webhooks, the first question is always the same: _what does Twilio actually send?_ The docs show an idealised payload, but the real request — its headers, its `X-Twilio-Signature` header, the exact JSON shape — is what your handler has to parse. A **Twilio webhook tester** gives you a public URL that captures those real requests so you can read every byte before you write any code. ## [Get a free Twilio webhook tester URL](#get-a-free-twilio-webhook-tester-url) The fastest way is our free [Webhook Bin](https://webhookrelay.com/blog/twilio-webhook-tester/webhook-bin/) — a no-code [webhook tester](https://webhookrelay.com/blog/twilio-webhook-tester/webhook-bin/) that gives you an instant public URL and stores every request that hits it, headers and body included. No signup, no deploy: 1. Open the [Webhook Bin](https://webhookrelay.com/blog/twilio-webhook-tester/webhook-bin/) and copy the URL it generates for you. 2. In **Console → Phone Numbers / Messaging Service webhook fields**, add a webhook endpoint and paste that URL. 3. Trigger an event (see below) and watch the request land in the bin in real time. Because the bin keeps the full request, you can inspect the `X-Twilio-Signature` header, the `Content-Type`, and the complete payload — the three things you need to build and verify a handler. ## [What a Twilio webhook looks like](#what-a-twilio-webhook-looks-like) Twilio delivers webhooks as an HTTP POST with a `application/x-www-form-urlencoded` body. Twilio webhooks are form-encoded, not JSON — and the signature is computed over the URL itself, which is exactly why testing against a stable, captured URL beats guessing from the docs. A typical `incoming SMS` payload looks like this: ``` { "MessageSid": "SM...", "From": "+15551234567", "To": "+15557654321", "Body": "Hello world", "NumMedia": "0" } ``` Common Twilio events you will want to test: - `incoming SMS` - `message status callbacks` - `voice call status` ## [Verifying the Twilio signature](#verifying-the-twilio-signature) Twilio signs each request so you can prove it really came from Twilio. The signature travels in the **`X-Twilio-Signature`** header and is base64 HMAC-SHA1 over the full URL plus sorted POST params, using your Twilio Auth Token. Capture a real request first, then use our [Twilio signature verifier](https://webhookrelay.com/blog/twilio-webhook-tester/verify-twilio-webhook-signature/) and the [verify a webhook signature](https://webhookrelay.com/blog/twilio-webhook-tester/blog/verify-webhook-signature/) guide to confirm your verification logic against a payload you can actually see. ## [From inspecting to receiving on localhost](#from-inspecting-to-receiving-on-localhost) A bin is perfect for _seeing_ the payload. When you are ready to drive your **local** handler with real Twilio events — without deploying — forward them straight to `localhost` with the Webhook Relay agent. The full walkthrough is here: [Receive Twilio webhooks on localhost](https://webhookrelay.com/blog/twilio-webhook-tester/blog/receive-twilio-webhooks-locally/). That gives you a stable public URL that tunnels to your machine, so Twilio keeps delivering to the same endpoint while you iterate on `localhost`, no firewall changes or public IP required. ## [Test Twilio webhooks online in three steps](#test-twilio-webhooks-online-in-three-steps) 1. **Capture** — point Twilio at a [Webhook Bin](https://webhookrelay.com/blog/twilio-webhook-tester/webhook-bin/) URL and inspect the real request. 2. **Verify** — confirm the `X-Twilio-Signature` header with the [Twilio signature verifier](https://webhookrelay.com/blog/twilio-webhook-tester/verify-twilio-webhook-signature/). 3. **Forward** — when the shape is clear, [receive Twilio webhooks on localhost](https://webhookrelay.com/blog/twilio-webhook-tester/blog/receive-twilio-webhooks-locally/) and build your handler. Want the full picture — setup, form-encoded payloads, `X-Twilio-Signature` validation and retries? See the [Twilio webhooks guide](https://webhookrelay.com/blog/twilio-webhook-tester/blog/twilio-webhooks-guide/). New to webhooks in general? Start with [what is a webhook](https://webhookrelay.com/blog/twilio-webhook-tester/blog/what-is-webhook/) and [how to test webhooks](https://webhookrelay.com/blog/twilio-webhook-tester/blog/how-to-test-webhooks/). Ready to inspect your first Twilio event? [Open a free Webhook Bin](https://webhookrelay.com/blog/twilio-webhook-tester/webhook-bin/) and paste the URL into Twilio. --- --- title: Shopify Webhook Tester — Test & Inspect Online meta: "og: title": "Shopify Webhook Tester — Test & Inspect Online" description: Test and inspect Shopify webhooks online with a free webhook tester URL — capture real Shopify payloads, read the signature header, then forward locally. url: https://webhookrelay.com/blog/shopify-webhook-tester.md file: /blog/shopify-webhook-tester.md --- ![Stripes](https://webhookrelay.com/blog/shopify-webhook-tester/images/stripes.svg) # **Shopify Webhook Tester — Test & Inspect Online** Test and inspect Shopify webhooks online with a free webhook tester URL — capture real Shopify payloads, read the signature header, then forward locally. ![Shopify Webhook Tester — Test & Inspect Shopify Webhooks Online](https://webhookrelay.com/blog/shopify-webhook-tester/images/blog/heroes/tester.jpg) If you are wiring up Shopify webhooks, the first question is always the same: _what does Shopify actually send?_ The docs show an idealised payload, but the real request — its headers, its `X-Shopify-Hmac-SHA256` header, the exact JSON shape — is what your handler has to parse. A **Shopify webhook tester** gives you a public URL that captures those real requests so you can read every byte before you write any code. ## [Get a free Shopify webhook tester URL](#get-a-free-shopify-webhook-tester-url) The fastest way is our free [Webhook Bin](https://webhookrelay.com/blog/shopify-webhook-tester/webhook-bin/) — a no-code [webhook tester](https://webhookrelay.com/blog/shopify-webhook-tester/webhook-bin/) that gives you an instant public URL and stores every request that hits it, headers and body included. No signup, no deploy: 1. Open the [Webhook Bin](https://webhookrelay.com/blog/shopify-webhook-tester/webhook-bin/) and copy the URL it generates for you. 2. In **Settings → Notifications → Webhooks (or the Admin API)**, add a webhook endpoint and paste that URL. 3. Trigger an event (see below) and watch the request land in the bin in real time. Because the bin keeps the full request, you can inspect the `X-Shopify-Hmac-SHA256` header, the `Content-Type`, and the complete payload — the three things you need to build and verify a handler. ## [What a Shopify webhook looks like](#what-a-shopify-webhook-looks-like) Shopify delivers webhooks as an HTTP POST with a `application/json` body. Shopify also sends `X-Shopify-Topic` (which event) and `X-Shopify-Shop-Domain` (which store) headers — capture them in your bin to confirm the topic and store before you write a single line of handler code. A typical `orders/create` payload looks like this: ``` { "id": 820982911946154500, "email": "jon@example.com", "financial_status": "paid", "total_price": "199.00", "currency": "USD", "line_items": [ { "title": "T-Shirt", "quantity": 1 } ] } ``` Common Shopify events you will want to test: - `orders/create` - `orders/paid` - `products/update` - `app/uninstalled` ## [Verifying the Shopify signature](#verifying-the-shopify-signature) Shopify signs each request so you can prove it really came from Shopify. The signature travels in the **`X-Shopify-Hmac-SHA256`** header and is base64-encoded HMAC-SHA256 of the raw body, using your app's API secret key. Capture a real request first, then use our [Shopify signature verifier](https://webhookrelay.com/blog/shopify-webhook-tester/verify-shopify-webhook-signature/) and the [verify a webhook signature](https://webhookrelay.com/blog/shopify-webhook-tester/blog/verify-webhook-signature/) guide to confirm your verification logic against a payload you can actually see. ## [From inspecting to receiving on localhost](#from-inspecting-to-receiving-on-localhost) A bin is perfect for _seeing_ the payload. When you are ready to drive your **local** handler with real Shopify events — without deploying — forward them straight to `localhost` with the Webhook Relay agent. The full walkthrough is here: [Receive Shopify webhooks on localhost](https://webhookrelay.com/blog/shopify-webhook-tester/blog/receiving-shopify-webhooks-flask-api/). That gives you a stable public URL that tunnels to your machine, so Shopify keeps delivering to the same endpoint while you iterate on `localhost`, no firewall changes or public IP required. ## [Test Shopify webhooks online in three steps](#test-shopify-webhooks-online-in-three-steps) 1. **Capture** — point Shopify at a [Webhook Bin](https://webhookrelay.com/blog/shopify-webhook-tester/webhook-bin/) URL and inspect the real request. 2. **Verify** — confirm the `X-Shopify-Hmac-SHA256` header with the [Shopify signature verifier](https://webhookrelay.com/blog/shopify-webhook-tester/verify-shopify-webhook-signature/). 3. **Forward** — when the shape is clear, [receive Shopify webhooks on localhost](https://webhookrelay.com/blog/shopify-webhook-tester/blog/receiving-shopify-webhooks-flask-api/) and build your handler. New to webhooks in general? Start with [what is a webhook](https://webhookrelay.com/blog/shopify-webhook-tester/blog/what-is-webhook/) and [how to test webhooks](https://webhookrelay.com/blog/shopify-webhook-tester/blog/how-to-test-webhooks/). Ready to inspect your first Shopify event? [Open a free Webhook Bin](https://webhookrelay.com/blog/shopify-webhook-tester/webhook-bin/) and paste the URL into Shopify. --- --- title: GitLab Webhook Tester — Test & Inspect Online | WebhookRelay meta: "og: title": "GitLab Webhook Tester — Test & Inspect Online" description: Test and inspect GitLab webhooks online with a free webhook tester URL — capture real GitLab payloads, read the signature header, then forward locally. url: https://webhookrelay.com/blog/gitlab-webhook-tester.md file: /blog/gitlab-webhook-tester.md --- ![Stripes](https://webhookrelay.com/blog/gitlab-webhook-tester/images/stripes.svg) # **GitLab Webhook Tester — Test & Inspect Online** Test and inspect GitLab webhooks online with a free webhook tester URL — capture real GitLab payloads, read the signature header, then forward locally. ![GitLab Webhook Tester — Test & Inspect GitLab Webhooks Online](https://webhookrelay.com/blog/gitlab-webhook-tester/images/blog/heroes/tester.jpg) If you are wiring up GitLab webhooks, the first question is always the same: _what does GitLab actually send?_ The docs show an idealised payload, but the real request — its headers, its `X-Gitlab-Token` header, the exact JSON shape — is what your handler has to parse. A **GitLab webhook tester** gives you a public URL that captures those real requests so you can read every byte before you write any code. ## [Get a free GitLab webhook tester URL](#get-a-free-gitlab-webhook-tester-url) The fastest way is our free [Webhook Bin](https://webhookrelay.com/blog/gitlab-webhook-tester/webhook-bin/) — a no-code [webhook tester](https://webhookrelay.com/blog/gitlab-webhook-tester/webhook-bin/) that gives you an instant public URL and stores every request that hits it, headers and body included. No signup, no deploy: 1. Open the [Webhook Bin](https://webhookrelay.com/blog/gitlab-webhook-tester/webhook-bin/) and copy the URL it generates for you. 2. In **Settings → Webhooks on a project or group**, add a webhook endpoint and paste that URL. 3. Trigger an event (see below) and watch the request land in the bin in real time. Because the bin keeps the full request, you can inspect the `X-Gitlab-Token` header, the `Content-Type`, and the complete payload — the three things you need to build and verify a handler. ## [What a GitLab webhook looks like](#what-a-gitlab-webhook-looks-like) GitLab delivers webhooks as an HTTP POST with a `application/json` body. GitLab identifies the event with an `X-Gitlab-Event` header (e.g. `Merge Request Hook`) and authenticates with a flat `X-Gitlab-Token` you compare verbatim — capture both to confirm your setup before writing handler logic. A typical `Push Hook` payload looks like this: ``` { "object_kind": "merge_request", "event_type": "merge_request", "object_attributes": { "iid": 1, "title": "Update README", "state": "opened", "action": "open" }, "project": { "path_with_namespace": "group/project" } } ``` Common GitLab events you will want to test: - `Push Hook` - `Merge Request Hook` - `Pipeline Hook` - `Issue Hook` ## [Verifying the GitLab signature](#verifying-the-gitlab-signature) GitLab signs each request so you can prove it really came from GitLab. The signature travels in the **`X-Gitlab-Token`** header and is a plain secret-token comparison (no HMAC), using the secret token you configured. Capture a real request first, then use our [HMAC signature verifier](https://webhookrelay.com/blog/gitlab-webhook-tester/hmac-verification/) and the [verify a webhook signature](https://webhookrelay.com/blog/gitlab-webhook-tester/blog/verify-webhook-signature/) guide to confirm your verification logic against a payload you can actually see. ## [From inspecting to receiving on localhost](#from-inspecting-to-receiving-on-localhost) A bin is perfect for _seeing_ the payload. When you are ready to drive your **local** handler with real GitLab events — without deploying — forward them straight to `localhost` with the Webhook Relay agent. The full walkthrough is here: [Receive GitLab webhooks on localhost](https://webhookrelay.com/blog/gitlab-webhook-tester/blog/receive-gitlab-webhooks-locally/). That gives you a stable public URL that tunnels to your machine, so GitLab keeps delivering to the same endpoint while you iterate on `localhost`, no firewall changes or public IP required. ## [Test GitLab webhooks online in three steps](#test-gitlab-webhooks-online-in-three-steps) 1. **Capture** — point GitLab at a [Webhook Bin](https://webhookrelay.com/blog/gitlab-webhook-tester/webhook-bin/) URL and inspect the real request. 2. **Verify** — confirm the `X-Gitlab-Token` header with the [HMAC verifier](https://webhookrelay.com/blog/gitlab-webhook-tester/hmac-verification/). 3. **Forward** — when the shape is clear, [receive GitLab webhooks on localhost](https://webhookrelay.com/blog/gitlab-webhook-tester/blog/receive-gitlab-webhooks-locally/) and build your handler. New to webhooks in general? Start with [what is a webhook](https://webhookrelay.com/blog/gitlab-webhook-tester/blog/what-is-webhook/) and [how to test webhooks](https://webhookrelay.com/blog/gitlab-webhook-tester/blog/how-to-test-webhooks/). Ready to inspect your first GitLab event? [Open a free Webhook Bin](https://webhookrelay.com/blog/gitlab-webhook-tester/webhook-bin/) and paste the URL into GitLab. --- --- title: Square Webhook Tester — Test & Inspect Online | WebhookRelay meta: "og: title": "Square Webhook Tester — Test & Inspect Online" description: Test and inspect Square webhooks online with a free webhook tester URL — capture real Square payloads, read the signature header, then forward locally. url: https://webhookrelay.com/blog/square-webhook-tester.md file: /blog/square-webhook-tester.md --- ![Stripes](https://webhookrelay.com/blog/square-webhook-tester/images/stripes.svg) # **Square Webhook Tester — Test & Inspect Online** Test and inspect Square webhooks online with a free webhook tester URL — capture real Square payloads, read the signature header, then forward locally. ![Square Webhook Tester — Test & Inspect Square Webhooks Online](https://webhookrelay.com/blog/square-webhook-tester/images/blog/heroes/tester.jpg) If you are wiring up Square webhooks, the first question is always the same: _what does Square actually send?_ The docs show an idealised payload, but the real request — its headers, its `x-square-hmacsha256-signature` header, the exact JSON shape — is what your handler has to parse. A **Square webhook tester** gives you a public URL that captures those real requests so you can read every byte before you write any code. ## [Get a free Square webhook tester URL](#get-a-free-square-webhook-tester-url) The fastest way is our free [Webhook Bin](https://webhookrelay.com/blog/square-webhook-tester/webhook-bin/) — a no-code [webhook tester](https://webhookrelay.com/blog/square-webhook-tester/webhook-bin/) that gives you an instant public URL and stores every request that hits it, headers and body included. No signup, no deploy: 1. Open the [Webhook Bin](https://webhookrelay.com/blog/square-webhook-tester/webhook-bin/) and copy the URL it generates for you. 2. In **Developer Dashboard → Webhooks → Subscriptions**, add a webhook endpoint and paste that URL. 3. Trigger an event (see below) and watch the request land in the bin in real time. Because the bin keeps the full request, you can inspect the `x-square-hmacsha256-signature` header, the `Content-Type`, and the complete payload — the three things you need to build and verify a handler. ## [What a Square webhook looks like](#what-a-square-webhook-looks-like) Square delivers webhooks as an HTTP POST with a `application/json` body. Square folds the destination URL into the signature, so a stable capture URL is essential — change the URL and the signature changes. Read the captured `type` field to see which event subscription fired. A typical `payment.created` payload looks like this: ``` { "merchant_id": "MLE...", "type": "payment.created", "event_id": "...", "data": { "type": "payment", "id": "...", "object": { "payment": { "id": "...", "amount_money": { "amount": 1000, "currency": "USD" }, "status": "COMPLETED" } } } } ``` Common Square events you will want to test: - `payment.created` - `payment.updated` - `order.updated` - `invoice.payment_made` ## [Verifying the Square signature](#verifying-the-square-signature) Square signs each request so you can prove it really came from Square. The signature travels in the **`x-square-hmacsha256-signature`** header and is base64 HMAC-SHA256 over the notification URL concatenated with the raw body, using the subscription's signature key. Capture a real request first, then use our [Square signature verifier](https://webhookrelay.com/blog/square-webhook-tester/verify-square-webhook-signature/) and the [verify a webhook signature](https://webhookrelay.com/blog/square-webhook-tester/blog/verify-webhook-signature/) guide to confirm your verification logic against a payload you can actually see. ## [From inspecting to receiving on localhost](#from-inspecting-to-receiving-on-localhost) A bin is perfect for _seeing_ the payload. When you are ready to drive your **local** handler with real Square events — without deploying — forward them straight to `localhost` with the Webhook Relay agent. The full walkthrough is here: [Receive Square webhooks on localhost](https://webhookrelay.com/blog/square-webhook-tester/blog/receive-square-webhooks-locally/). That gives you a stable public URL that tunnels to your machine, so Square keeps delivering to the same endpoint while you iterate on `localhost`, no firewall changes or public IP required. ## [Test Square webhooks online in three steps](#test-square-webhooks-online-in-three-steps) 1. **Capture** — point Square at a [Webhook Bin](https://webhookrelay.com/blog/square-webhook-tester/webhook-bin/) URL and inspect the real request. 2. **Verify** — confirm the `x-square-hmacsha256-signature` header with the [Square signature verifier](https://webhookrelay.com/blog/square-webhook-tester/verify-square-webhook-signature/). 3. **Forward** — when the shape is clear, [receive Square webhooks on localhost](https://webhookrelay.com/blog/square-webhook-tester/blog/receive-square-webhooks-locally/) and build your handler. New to webhooks in general? Start with [what is a webhook](https://webhookrelay.com/blog/square-webhook-tester/blog/what-is-webhook/) and [how to test webhooks](https://webhookrelay.com/blog/square-webhook-tester/blog/how-to-test-webhooks/). Ready to inspect your first Square event? [Open a free Webhook Bin](https://webhookrelay.com/blog/square-webhook-tester/webhook-bin/) and paste the URL into Square. --- --- title: PayPal Webhook Tester — Test & Inspect Online | WebhookRelay meta: "og: title": "PayPal Webhook Tester — Test & Inspect Online" description: Test and inspect PayPal webhooks online with a free webhook tester URL — capture real PayPal payloads, read the signature header, then forward locally. url: https://webhookrelay.com/blog/paypal-webhook-tester.md file: /blog/paypal-webhook-tester.md --- ![Stripes](https://webhookrelay.com/blog/paypal-webhook-tester/images/stripes.svg) # **PayPal Webhook Tester — Test & Inspect Online** Test and inspect PayPal webhooks online with a free webhook tester URL — capture real PayPal payloads, read the signature header, then forward locally. ![PayPal Webhook Tester — Test & Inspect PayPal Webhooks Online](https://webhookrelay.com/blog/paypal-webhook-tester/images/blog/heroes/tester.jpg) If you are wiring up PayPal webhooks, the first question is always the same: _what does PayPal actually send?_ The docs show an idealised payload, but the real request — its headers, its `Paypal-Transmission-Sig` header, the exact JSON shape — is what your handler has to parse. A **PayPal webhook tester** gives you a public URL that captures those real requests so you can read every byte before you write any code. ## [Get a free PayPal webhook tester URL](#get-a-free-paypal-webhook-tester-url) The fastest way is our free [Webhook Bin](https://webhookrelay.com/blog/paypal-webhook-tester/webhook-bin/) — a no-code [webhook tester](https://webhookrelay.com/blog/paypal-webhook-tester/webhook-bin/) that gives you an instant public URL and stores every request that hits it, headers and body included. No signup, no deploy: 1. Open the [Webhook Bin](https://webhookrelay.com/blog/paypal-webhook-tester/webhook-bin/) and copy the URL it generates for you. 2. In **Developer Dashboard → My Apps & Credentials → Webhooks**, add a webhook endpoint and paste that URL. 3. Trigger an event (see below) and watch the request land in the bin in real time. Because the bin keeps the full request, you can inspect the `Paypal-Transmission-Sig` header, the `Content-Type`, and the complete payload — the three things you need to build and verify a handler. ## [What a PayPal webhook looks like](#what-a-paypal-webhook-looks-like) PayPal delivers webhooks as an HTTP POST with a `application/json` body. PayPal verification is unusual: it sends several `Paypal-Transmission-*` headers and a cert URL rather than a simple HMAC. Capturing all of them in one place is the fastest way to understand what your verification code has to work with. A typical `PAYMENT.CAPTURE.COMPLETED` payload looks like this: ``` { "id": "WH-...", "event_type": "PAYMENT.CAPTURE.COMPLETED", "resource": { "id": "...", "amount": { "value": "19.99", "currency_code": "USD" }, "status": "COMPLETED" } } ``` Common PayPal events you will want to test: - `PAYMENT.CAPTURE.COMPLETED` - `CHECKOUT.ORDER.APPROVED` - `BILLING.SUBSCRIPTION.ACTIVATED` ## [Verifying the PayPal signature](#verifying-the-paypal-signature) PayPal signs each request so you can prove it really came from PayPal. The signature travels in the **`Paypal-Transmission-Sig`** header and is a certificate-based signature (verified with `Paypal-Cert-Url` and `Paypal-Transmission-Id`, not a shared-secret HMAC), using PayPal's published certificate plus your webhook ID. Capture a real request first, then use our [HMAC signature verifier](https://webhookrelay.com/blog/paypal-webhook-tester/hmac-verification/) and the [verify a webhook signature](https://webhookrelay.com/blog/paypal-webhook-tester/blog/verify-webhook-signature/) guide to confirm your verification logic against a payload you can actually see. ## [From inspecting to receiving on localhost](#from-inspecting-to-receiving-on-localhost) A bin is perfect for _seeing_ the payload. When you are ready to drive your **local** handler with real PayPal events — without deploying — forward them straight to `localhost` with the Webhook Relay agent. The full walkthrough is here: [Receive PayPal webhooks on localhost](https://webhookrelay.com/blog/paypal-webhook-tester/blog/receiving-paypal-webhooks-localhost/). That gives you a stable public URL that tunnels to your machine, so PayPal keeps delivering to the same endpoint while you iterate on `localhost`, no firewall changes or public IP required. ## [Test PayPal webhooks online in three steps](#test-paypal-webhooks-online-in-three-steps) 1. **Capture** — point PayPal at a [Webhook Bin](https://webhookrelay.com/blog/paypal-webhook-tester/webhook-bin/) URL and inspect the real request. 2. **Verify** — confirm the `Paypal-Transmission-Sig` header with the [HMAC verifier](https://webhookrelay.com/blog/paypal-webhook-tester/hmac-verification/). 3. **Forward** — when the shape is clear, [receive PayPal webhooks on localhost](https://webhookrelay.com/blog/paypal-webhook-tester/blog/receiving-paypal-webhooks-localhost/) and build your handler. New to webhooks in general? Start with [what is a webhook](https://webhookrelay.com/blog/paypal-webhook-tester/blog/what-is-webhook/) and [how to test webhooks](https://webhookrelay.com/blog/paypal-webhook-tester/blog/how-to-test-webhooks/). Ready to inspect your first PayPal event? [Open a free Webhook Bin](https://webhookrelay.com/blog/paypal-webhook-tester/webhook-bin/) and paste the URL into PayPal. --- --- title: Sentry Webhook Tester — Test & Inspect Online | WebhookRelay meta: "og: title": "Sentry Webhook Tester — Test & Inspect Online" description: Test and inspect Sentry webhooks online with a free webhook tester URL — capture real Sentry payloads, read the signature header, then forward locally. url: https://webhookrelay.com/blog/sentry-webhook-tester.md file: /blog/sentry-webhook-tester.md --- ![Stripes](https://webhookrelay.com/blog/sentry-webhook-tester/images/stripes.svg) # **Sentry Webhook Tester — Test & Inspect Online** Test and inspect Sentry webhooks online with a free webhook tester URL — capture real Sentry payloads, read the signature header, then forward locally. ![Sentry Webhook Tester — Test & Inspect Sentry Webhooks Online](https://webhookrelay.com/blog/sentry-webhook-tester/images/blog/heroes/tester.jpg) If you are wiring up Sentry webhooks, the first question is always the same: _what does Sentry actually send?_ The docs show an idealised payload, but the real request — its headers, its `Sentry-Hook-Signature` header, the exact JSON shape — is what your handler has to parse. A **Sentry webhook tester** gives you a public URL that captures those real requests so you can read every byte before you write any code. ## [Get a free Sentry webhook tester URL](#get-a-free-sentry-webhook-tester-url) The fastest way is our free [Webhook Bin](https://webhookrelay.com/blog/sentry-webhook-tester/webhook-bin/) — a no-code [webhook tester](https://webhookrelay.com/blog/sentry-webhook-tester/webhook-bin/) that gives you an instant public URL and stores every request that hits it, headers and body included. No signup, no deploy: 1. Open the [Webhook Bin](https://webhookrelay.com/blog/sentry-webhook-tester/webhook-bin/) and copy the URL it generates for you. 2. In **Settings → Developer Settings → Internal Integration → Webhooks**, add a webhook endpoint and paste that URL. 3. Trigger an event (see below) and watch the request land in the bin in real time. Because the bin keeps the full request, you can inspect the `Sentry-Hook-Signature` header, the `Content-Type`, and the complete payload — the three things you need to build and verify a handler. ## [What a Sentry webhook looks like](#what-a-sentry-webhook-looks-like) Sentry delivers webhooks as an HTTP POST with a `application/json` body. Sentry sends a `Sentry-Hook-Resource` header naming the resource (issue, error, comment) alongside the signature — capturing it tells you exactly which subscription delivered the payload. A typical `issue.created` payload looks like this: ``` { "action": "created", "installation": { "uuid": "..." }, "data": { "issue": { "id": "...", "title": "TypeError: undefined is not a function", "culprit": "app/main", "level": "error" } } } ``` Common Sentry events you will want to test: - `issue.created` - `issue.assigned` - `error.created` - `comment.created` ## [Verifying the Sentry signature](#verifying-the-sentry-signature) Sentry signs each request so you can prove it really came from Sentry. The signature travels in the **`Sentry-Hook-Signature`** header and is HMAC-SHA256 of the raw request body, using your integration's client secret. Capture a real request first, then use our [HMAC signature verifier](https://webhookrelay.com/blog/sentry-webhook-tester/hmac-verification/) and the [verify a webhook signature](https://webhookrelay.com/blog/sentry-webhook-tester/blog/verify-webhook-signature/) guide to confirm your verification logic against a payload you can actually see. ## [From inspecting to receiving on localhost](#from-inspecting-to-receiving-on-localhost) A bin is perfect for _seeing_ the payload. When you are ready to drive your **local** handler with real Sentry events — without deploying — forward them straight to `localhost` with the Webhook Relay agent. The full walkthrough is here: [Receive Sentry webhooks on localhost](https://webhookrelay.com/blog/sentry-webhook-tester/blog/receive-sentry-webhooks-locally/). That gives you a stable public URL that tunnels to your machine, so Sentry keeps delivering to the same endpoint while you iterate on `localhost`, no firewall changes or public IP required. ## [Test Sentry webhooks online in three steps](#test-sentry-webhooks-online-in-three-steps) 1. **Capture** — point Sentry at a [Webhook Bin](https://webhookrelay.com/blog/sentry-webhook-tester/webhook-bin/) URL and inspect the real request. 2. **Verify** — confirm the `Sentry-Hook-Signature` header with the [HMAC verifier](https://webhookrelay.com/blog/sentry-webhook-tester/hmac-verification/). 3. **Forward** — when the shape is clear, [receive Sentry webhooks on localhost](https://webhookrelay.com/blog/sentry-webhook-tester/blog/receive-sentry-webhooks-locally/) and build your handler. New to webhooks in general? Start with [what is a webhook](https://webhookrelay.com/blog/sentry-webhook-tester/blog/what-is-webhook/) and [how to test webhooks](https://webhookrelay.com/blog/sentry-webhook-tester/blog/how-to-test-webhooks/). Ready to inspect your first Sentry event? [Open a free Webhook Bin](https://webhookrelay.com/blog/sentry-webhook-tester/webhook-bin/) and paste the URL into Sentry. --- --- title: Slack Webhook Tester — Test & Inspect Online | WebhookRelay meta: "og: title": "Slack Webhook Tester — Test & Inspect Online" description: Test and inspect Slack webhooks online with a free webhook tester URL — capture real Slack payloads, read the signature header, then forward locally. url: https://webhookrelay.com/blog/slack-webhook-tester.md file: /blog/slack-webhook-tester.md --- ![Stripes](https://webhookrelay.com/blog/slack-webhook-tester/images/stripes.svg) # **Slack Webhook Tester — Test & Inspect Online** Test and inspect Slack webhooks online with a free webhook tester URL — capture real Slack payloads, read the signature header, then forward locally. ![Slack Webhook Tester — Test & Inspect Slack Webhooks Online](https://webhookrelay.com/blog/slack-webhook-tester/images/blog/heroes/tester.jpg) If you are wiring up Slack webhooks, the first question is always the same: _what does Slack actually send?_ The docs show an idealised payload, but the real request — its headers, its `X-Slack-Signature` header, the exact JSON shape — is what your handler has to parse. A **Slack webhook tester** gives you a public URL that captures those real requests so you can read every byte before you write any code. ## [Get a free Slack webhook tester URL](#get-a-free-slack-webhook-tester-url) The fastest way is our free [Webhook Bin](https://webhookrelay.com/blog/slack-webhook-tester/webhook-bin/) — a no-code [webhook tester](https://webhookrelay.com/blog/slack-webhook-tester/webhook-bin/) that gives you an instant public URL and stores every request that hits it, headers and body included. No signup, no deploy: 1. Open the [Webhook Bin](https://webhookrelay.com/blog/slack-webhook-tester/webhook-bin/) and copy the URL it generates for you. 2. In **api.slack.com/apps → your app → Event Subscriptions**, add a webhook endpoint and paste that URL. 3. Trigger an event (see below) and watch the request land in the bin in real time. Because the bin keeps the full request, you can inspect the `X-Slack-Signature` header, the `Content-Type`, and the complete payload — the three things you need to build and verify a handler. ## [What a Slack webhook looks like](#what-a-slack-webhook-looks-like) Slack delivers webhooks as an HTTP POST with a `application/json` body. Before any events arrive, Slack sends a one-time `url_verification` challenge that your endpoint must echo back. Capturing it in a bin lets you see the exact `challenge` value Slack expects — a common first-time stumbling block. A typical `app_mention` payload looks like this: ``` { "type": "event_callback", "team_id": "T...", "event": { "type": "app_mention", "user": "U...", "text": "<@U...> hello", "channel": "C..." } } ``` Common Slack events you will want to test: - `app_mention` - `message.channels` - `reaction_added` - `url_verification` ## [Verifying the Slack signature](#verifying-the-slack-signature) Slack signs each request so you can prove it really came from Slack. The signature travels in the **`X-Slack-Signature`** header and is `v0=` HMAC-SHA256 over `v0:timestamp:body`, paired with the `X-Slack-Request-Timestamp` header, using your app's Signing Secret. Capture a real request first, then use our [Slack signature verifier](https://webhookrelay.com/blog/slack-webhook-tester/verify-slack-webhook-signature/) and the [verify a webhook signature](https://webhookrelay.com/blog/slack-webhook-tester/blog/verify-webhook-signature/) guide to confirm your verification logic against a payload you can actually see. ## [From inspecting to receiving on localhost](#from-inspecting-to-receiving-on-localhost) A bin is perfect for _seeing_ the payload. When you are ready to drive your **local** handler with real Slack events — without deploying — forward them straight to `localhost` with the Webhook Relay agent. The full walkthrough is here: [Receive Slack webhooks on localhost](https://webhookrelay.com/blog/slack-webhook-tester/blog/receive-slack-events-locally/). That gives you a stable public URL that tunnels to your machine, so Slack keeps delivering to the same endpoint while you iterate on `localhost`, no firewall changes or public IP required. ## [Test Slack webhooks online in three steps](#test-slack-webhooks-online-in-three-steps) 1. **Capture** — point Slack at a [Webhook Bin](https://webhookrelay.com/blog/slack-webhook-tester/webhook-bin/) URL and inspect the real request. 2. **Verify** — confirm the `X-Slack-Signature` header with the [Slack signature verifier](https://webhookrelay.com/blog/slack-webhook-tester/verify-slack-webhook-signature/). 3. **Forward** — when the shape is clear, [receive Slack webhooks on localhost](https://webhookrelay.com/blog/slack-webhook-tester/blog/receive-slack-events-locally/) and build your handler. New to webhooks in general? Start with [what is a webhook](https://webhookrelay.com/blog/slack-webhook-tester/blog/what-is-webhook/) and [how to test webhooks](https://webhookrelay.com/blog/slack-webhook-tester/blog/how-to-test-webhooks/). Ready to inspect your first Slack event? [Open a free Webhook Bin](https://webhookrelay.com/blog/slack-webhook-tester/webhook-bin/) and paste the URL into Slack. --- --- title: HubSpot Webhook Tester — Test & Inspect Online meta: "og: title": "HubSpot Webhook Tester — Test & Inspect Online" description: Test and inspect HubSpot webhooks online with a free webhook tester URL — capture real HubSpot payloads, read the signature header, then forward locally. url: https://webhookrelay.com/blog/hubspot-webhook-tester.md file: /blog/hubspot-webhook-tester.md --- ![Stripes](https://webhookrelay.com/blog/hubspot-webhook-tester/images/stripes.svg) # **HubSpot Webhook Tester — Test & Inspect Online** Test and inspect HubSpot webhooks online with a free webhook tester URL — capture real HubSpot payloads, read the signature header, then forward locally. ![HubSpot Webhook Tester](https://webhookrelay.com/blog/hubspot-webhook-tester/images/blog/heroes/tester.jpg) If you are wiring up HubSpot webhooks, the first question is always the same: _what does HubSpot actually send?_ The docs show an idealised payload, but the real request — its headers, its `X-HubSpot-Signature-v3` header, the exact JSON shape — is what your handler has to parse. A **HubSpot webhook tester** gives you a public URL that captures those real requests so you can read every byte before you write any code. ## [Get a free HubSpot webhook tester URL](#get-a-free-hubspot-webhook-tester-url) The fastest way is our free [Webhook Bin](https://webhookrelay.com/blog/hubspot-webhook-tester/webhook-bin/) — a no-code [webhook tester](https://webhookrelay.com/blog/hubspot-webhook-tester/webhook-bin/) that gives you an instant public URL and stores every request that hits it, headers and body included. No signup, no deploy: 1. Open the [Webhook Bin](https://webhookrelay.com/blog/hubspot-webhook-tester/webhook-bin/) and copy the URL it generates for you. 2. In **your app's Webhooks settings in the HubSpot developer account**, add a webhook endpoint and paste that URL. 3. Trigger an event (see below) and watch the request land in the bin in real time. Because the bin keeps the full request, you can inspect the `X-HubSpot-Signature-v3` header, the `Content-Type`, and the complete payload — the three things you need to build and verify a handler. ## [What a HubSpot webhook looks like](#what-a-hubspot-webhook-looks-like) HubSpot delivers webhooks as an HTTP POST with a `application/json` body. HubSpot batches events into a JSON array and signs across the method, URL, body and the `X-HubSpot-Request-Timestamp` header — so the captured URL and timestamp matter as much as the body. A typical `contact.creation` payload looks like this: ``` [ { "eventId": 12345, "subscriptionType": "contact.creation", "objectId": 98765, "propertyName": "email", "occurredAt": 1700000000000 } ] ``` Common HubSpot events you will want to test: - `contact.creation` - `contact.propertyChange` - `deal.creation` - `company.creation` ## [Verifying the HubSpot signature](#verifying-the-hubspot-signature) HubSpot signs each request so you can prove it really came from HubSpot. The signature travels in the **`X-HubSpot-Signature-v3`** header and is HMAC-SHA256 over the HTTP method, URI, body and timestamp, using your app's client secret. Capture a real request first, then use our [HMAC signature verifier](https://webhookrelay.com/blog/hubspot-webhook-tester/hmac-verification/) and the [verify a webhook signature](https://webhookrelay.com/blog/hubspot-webhook-tester/blog/verify-webhook-signature/) guide to confirm your verification logic against a payload you can actually see. ## [From inspecting to receiving on localhost](#from-inspecting-to-receiving-on-localhost) A bin is perfect for _seeing_ the payload. When you are ready to drive your **local** handler with real HubSpot events — without deploying — forward them straight to `localhost` with the Webhook Relay agent. The full walkthrough is here: [Receive HubSpot webhooks on localhost](https://webhookrelay.com/blog/hubspot-webhook-tester/blog/receive-hubspot-webhooks-locally/). That gives you a stable public URL that tunnels to your machine, so HubSpot keeps delivering to the same endpoint while you iterate on `localhost`, no firewall changes or public IP required. ## [Test HubSpot webhooks online in three steps](#test-hubspot-webhooks-online-in-three-steps) 1. **Capture** — point HubSpot at a [Webhook Bin](https://webhookrelay.com/blog/hubspot-webhook-tester/webhook-bin/) URL and inspect the real request. 2. **Verify** — confirm the `X-HubSpot-Signature-v3` header with the [HMAC verifier](https://webhookrelay.com/blog/hubspot-webhook-tester/hmac-verification/). 3. **Forward** — when the shape is clear, [receive HubSpot webhooks on localhost](https://webhookrelay.com/blog/hubspot-webhook-tester/blog/receive-hubspot-webhooks-locally/) and build your handler. New to webhooks in general? Start with [what is a webhook](https://webhookrelay.com/blog/hubspot-webhook-tester/blog/what-is-webhook/) and [how to test webhooks](https://webhookrelay.com/blog/hubspot-webhook-tester/blog/how-to-test-webhooks/). Ready to inspect your first HubSpot event? [Open a free Webhook Bin](https://webhookrelay.com/blog/hubspot-webhook-tester/webhook-bin/) and paste the URL into HubSpot. --- --- title: Vercel Webhook Tester — Test & Inspect Online | WebhookRelay meta: "og: title": "Vercel Webhook Tester — Test & Inspect Online" description: Test and inspect Vercel webhooks online with a free webhook tester URL — capture real Vercel payloads, read the signature header, then forward locally. url: https://webhookrelay.com/blog/vercel-webhook-tester.md file: /blog/vercel-webhook-tester.md --- ![Stripes](https://webhookrelay.com/blog/vercel-webhook-tester/images/stripes.svg) # **Vercel Webhook Tester — Test & Inspect Online** Test and inspect Vercel webhooks online with a free webhook tester URL — capture real Vercel payloads, read the signature header, then forward locally. ![Vercel Webhook Tester](https://webhookrelay.com/blog/vercel-webhook-tester/images/blog/heroes/tester.jpg) If you are wiring up Vercel webhooks, the first question is always the same: _what does Vercel actually send?_ The docs show an idealised payload, but the real request — its headers, its `x-vercel-signature` header, the exact JSON shape — is what your handler has to parse. A **Vercel webhook tester** gives you a public URL that captures those real requests so you can read every byte before you write any code. ## [Get a free Vercel webhook tester URL](#get-a-free-vercel-webhook-tester-url) The fastest way is our free [Webhook Bin](https://webhookrelay.com/blog/vercel-webhook-tester/webhook-bin/) — a no-code [webhook tester](https://webhookrelay.com/blog/vercel-webhook-tester/webhook-bin/) that gives you an instant public URL and stores every request that hits it, headers and body included. No signup, no deploy: 1. Open the [Webhook Bin](https://webhookrelay.com/blog/vercel-webhook-tester/webhook-bin/) and copy the URL it generates for you. 2. In **your Team or integration settings → Webhooks in the Vercel dashboard**, add a webhook endpoint and paste that URL. 3. Trigger an event (see below) and watch the request land in the bin in real time. Because the bin keeps the full request, you can inspect the `x-vercel-signature` header, the `Content-Type`, and the complete payload — the three things you need to build and verify a handler. ## [What a Vercel webhook looks like](#what-a-vercel-webhook-looks-like) Vercel delivers webhooks as an HTTP POST with a `application/json` body. Vercel signs the raw body and also sends the event type in the payload's `type` field — capture both so you can route on the event and verify the `x-vercel-signature` digest. A typical `deployment.created` payload looks like this: ``` { "id": "evt_...", "type": "deployment.succeeded", "createdAt": 1700000000000, "payload": { "deployment": { "id": "dpl_...", "url": "my-app.vercel.app", "state": "READY" }, "project": { "id": "prj_..." } } } ``` Common Vercel events you will want to test: - `deployment.created` - `deployment.succeeded` - `deployment.error` - `project.created` ## [Verifying the Vercel signature](#verifying-the-vercel-signature) Vercel signs each request so you can prove it really came from Vercel. The signature travels in the **`x-vercel-signature`** header and is HMAC-SHA1 of the raw request body, using your integration's client secret. Capture a real request first, then use our [HMAC signature verifier](https://webhookrelay.com/blog/vercel-webhook-tester/hmac-verification/) and the [verify a webhook signature](https://webhookrelay.com/blog/vercel-webhook-tester/blog/verify-webhook-signature/) guide to confirm your verification logic against a payload you can actually see. ## [From inspecting to receiving on localhost](#from-inspecting-to-receiving-on-localhost) A bin is perfect for _seeing_ the payload. When you are ready to drive your **local** handler with real Vercel events — without deploying — forward them straight to `localhost` with the Webhook Relay agent. The full walkthrough is here: [Receive Vercel webhooks on localhost](https://webhookrelay.com/blog/vercel-webhook-tester/blog/receive-vercel-webhooks-locally/). That gives you a stable public URL that tunnels to your machine, so Vercel keeps delivering to the same endpoint while you iterate on `localhost`, no firewall changes or public IP required. ## [Test Vercel webhooks online in three steps](#test-vercel-webhooks-online-in-three-steps) 1. **Capture** — point Vercel at a [Webhook Bin](https://webhookrelay.com/blog/vercel-webhook-tester/webhook-bin/) URL and inspect the real request. 2. **Verify** — confirm the `x-vercel-signature` header with the [HMAC verifier](https://webhookrelay.com/blog/vercel-webhook-tester/hmac-verification/). 3. **Forward** — when the shape is clear, [receive Vercel webhooks on localhost](https://webhookrelay.com/blog/vercel-webhook-tester/blog/receive-vercel-webhooks-locally/) and build your handler. New to webhooks in general? Start with [what is a webhook](https://webhookrelay.com/blog/vercel-webhook-tester/blog/what-is-webhook/) and [how to test webhooks](https://webhookrelay.com/blog/vercel-webhook-tester/blog/how-to-test-webhooks/). Ready to inspect your first Vercel event? [Open a free Webhook Bin](https://webhookrelay.com/blog/vercel-webhook-tester/webhook-bin/) and paste the URL into Vercel. --- --- title: Clerk Webhook Tester — Test & Inspect Online | WebhookRelay meta: "og: title": "Clerk Webhook Tester — Test & Inspect Online" description: Test and inspect Clerk webhooks online with a free webhook tester URL — capture real Clerk payloads, read the signature header, then forward locally. url: https://webhookrelay.com/blog/clerk-webhook-tester.md file: /blog/clerk-webhook-tester.md --- ![Stripes](https://webhookrelay.com/blog/clerk-webhook-tester/images/stripes.svg) # **Clerk Webhook Tester — Test & Inspect Online** Test and inspect Clerk webhooks online with a free webhook tester URL — capture real Clerk payloads, read the signature header, then forward locally. ![Clerk Webhook Tester — Test & Inspect Clerk Webhooks Online](https://webhookrelay.com/blog/clerk-webhook-tester/images/blog/heroes/tester.jpg) If you are wiring up Clerk webhooks, the first question is always the same: _what does Clerk actually send?_ The docs show an idealised payload, but the real request — its headers, its `svix-signature` header, the exact JSON shape — is what your handler has to parse. A **Clerk webhook tester** gives you a public URL that captures those real requests so you can read every byte before you write any code. ## [Get a free Clerk webhook tester URL](#get-a-free-clerk-webhook-tester-url) The fastest way is our free [Webhook Bin](https://webhookrelay.com/blog/clerk-webhook-tester/webhook-bin/) — a no-code [webhook tester](https://webhookrelay.com/blog/clerk-webhook-tester/webhook-bin/) that gives you an instant public URL and stores every request that hits it, headers and body included. No signup, no deploy: 1. Open the [Webhook Bin](https://webhookrelay.com/blog/clerk-webhook-tester/webhook-bin/) and copy the URL it generates for you. 2. In **Clerk Dashboard → Webhooks (powered by Svix)**, add a webhook endpoint and paste that URL. 3. Trigger an event (see below) and watch the request land in the bin in real time. Because the bin keeps the full request, you can inspect the `svix-signature` header, the `Content-Type`, and the complete payload — the three things you need to build and verify a handler. ## [What a Clerk webhook looks like](#what-a-clerk-webhook-looks-like) Clerk delivers webhooks as an HTTP POST with a `application/json` body. Clerk uses Svix under the hood, so the signature scheme (`svix-id`, `svix-timestamp`, `svix-signature`) is identical to many other Svix-powered providers — learn it once on a captured payload and it transfers everywhere. A typical `user.created` payload looks like this: ``` { "type": "user.created", "object": "event", "data": { "id": "user_...", "email_addresses": [ { "email_address": "jon@example.com" } ], "first_name": "Jon" } } ``` Common Clerk events you will want to test: - `user.created` - `user.updated` - `session.created` - `organization.created` ## [Verifying the Clerk signature](#verifying-the-clerk-signature) Clerk signs each request so you can prove it really came from Clerk. The signature travels in the **`svix-signature`** header and is base64 HMAC-SHA256 over `svix-id.svix-timestamp.body`, with `svix-id` and `svix-timestamp` headers, using the signing secret that starts with `whsec_`. Capture a real request first, then use our [HMAC signature verifier](https://webhookrelay.com/blog/clerk-webhook-tester/hmac-verification/) and the [verify a webhook signature](https://webhookrelay.com/blog/clerk-webhook-tester/blog/verify-webhook-signature/) guide to confirm your verification logic against a payload you can actually see. ## [From inspecting to receiving on localhost](#from-inspecting-to-receiving-on-localhost) A bin is perfect for _seeing_ the payload. When you are ready to drive your **local** handler with real Clerk events — without deploying — forward them straight to `localhost` with the Webhook Relay agent. The full walkthrough is here: [Receive Clerk webhooks on localhost](https://webhookrelay.com/blog/clerk-webhook-tester/blog/receive-clerk-webhooks-locally/). That gives you a stable public URL that tunnels to your machine, so Clerk keeps delivering to the same endpoint while you iterate on `localhost`, no firewall changes or public IP required. ## [Test Clerk webhooks online in three steps](#test-clerk-webhooks-online-in-three-steps) 1. **Capture** — point Clerk at a [Webhook Bin](https://webhookrelay.com/blog/clerk-webhook-tester/webhook-bin/) URL and inspect the real request. 2. **Verify** — confirm the `svix-signature` header with the [HMAC verifier](https://webhookrelay.com/blog/clerk-webhook-tester/hmac-verification/). 3. **Forward** — when the shape is clear, [receive Clerk webhooks on localhost](https://webhookrelay.com/blog/clerk-webhook-tester/blog/receive-clerk-webhooks-locally/) and build your handler. New to webhooks in general? Start with [what is a webhook](https://webhookrelay.com/blog/clerk-webhook-tester/blog/what-is-webhook/) and [how to test webhooks](https://webhookrelay.com/blog/clerk-webhook-tester/blog/how-to-test-webhooks/). Ready to inspect your first Clerk event? [Open a free Webhook Bin](https://webhookrelay.com/blog/clerk-webhook-tester/webhook-bin/) and paste the URL into Clerk. --- --- title: Linear Webhook Tester — Test & Inspect Online | WebhookRelay meta: "og: title": "Linear Webhook Tester — Test & Inspect Online" description: Test and inspect Linear webhooks online with a free webhook tester URL — capture real Linear payloads, read the signature header, then forward locally. url: https://webhookrelay.com/blog/linear-webhook-tester.md file: /blog/linear-webhook-tester.md --- ![Stripes](https://webhookrelay.com/blog/linear-webhook-tester/images/stripes.svg) # **Linear Webhook Tester — Test & Inspect Online** Test and inspect Linear webhooks online with a free webhook tester URL — capture real Linear payloads, read the signature header, then forward locally. ![Linear Webhook Tester](https://webhookrelay.com/blog/linear-webhook-tester/images/blog/heroes/tester.jpg) If you are wiring up Linear webhooks, the first question is always the same: _what does Linear actually send?_ The docs show an idealised payload, but the real request — its headers, its `Linear-Signature` header, the exact JSON shape — is what your handler has to parse. A **Linear webhook tester** gives you a public URL that captures those real requests so you can read every byte before you write any code. ## [Get a free Linear webhook tester URL](#get-a-free-linear-webhook-tester-url) The fastest way is our free [Webhook Bin](https://webhookrelay.com/blog/linear-webhook-tester/webhook-bin/) — a no-code [webhook tester](https://webhookrelay.com/blog/linear-webhook-tester/webhook-bin/) that gives you an instant public URL and stores every request that hits it, headers and body included. No signup, no deploy: 1. Open the [Webhook Bin](https://webhookrelay.com/blog/linear-webhook-tester/webhook-bin/) and copy the URL it generates for you. 2. In **Settings → API → Webhooks in Linear**, add a webhook endpoint and paste that URL. 3. Trigger an event (see below) and watch the request land in the bin in real time. Because the bin keeps the full request, you can inspect the `Linear-Signature` header, the `Content-Type`, and the complete payload — the three things you need to build and verify a handler. ## [What a Linear webhook looks like](#what-a-linear-webhook-looks-like) Linear delivers webhooks as an HTTP POST with a `application/json` body. Linear sends an `action` (create/update/remove) and a `type` (Issue, Comment, Project) in the body, and signs the raw payload — capture a real event to see the nested data object for the resource you care about. A typical `Issue create` payload looks like this: ``` { "action": "create", "type": "Issue", "data": { "id": "...", "title": "Fix login bug", "state": { "name": "Todo" }, "team": { "key": "ENG" } }, "createdAt": "2026-06-11T15:00:00.000Z" } ``` Common Linear events you will want to test: - `Issue create` - `Issue update` - `Comment create` - `Project update` ## [Verifying the Linear signature](#verifying-the-linear-signature) Linear signs each request so you can prove it really came from Linear. The signature travels in the **`Linear-Signature`** header and is HMAC-SHA256 of the raw request body, using the webhook signing secret Linear shows when you create the webhook. Capture a real request first, then use our [HMAC signature verifier](https://webhookrelay.com/blog/linear-webhook-tester/hmac-verification/) and the [verify a webhook signature](https://webhookrelay.com/blog/linear-webhook-tester/blog/verify-webhook-signature/) guide to confirm your verification logic against a payload you can actually see. ## [From inspecting to receiving on localhost](#from-inspecting-to-receiving-on-localhost) A bin is perfect for _seeing_ the payload. When you are ready to drive your **local** handler with real Linear events — without deploying — forward them straight to `localhost` with the Webhook Relay agent. The full walkthrough is here: [Receive Linear webhooks on localhost](https://webhookrelay.com/blog/linear-webhook-tester/blog/receive-linear-webhooks-locally/). That gives you a stable public URL that tunnels to your machine, so Linear keeps delivering to the same endpoint while you iterate on `localhost`, no firewall changes or public IP required. ## [Test Linear webhooks online in three steps](#test-linear-webhooks-online-in-three-steps) 1. **Capture** — point Linear at a [Webhook Bin](https://webhookrelay.com/blog/linear-webhook-tester/webhook-bin/) URL and inspect the real request. 2. **Verify** — confirm the `Linear-Signature` header with the [HMAC verifier](https://webhookrelay.com/blog/linear-webhook-tester/hmac-verification/). 3. **Forward** — when the shape is clear, [receive Linear webhooks on localhost](https://webhookrelay.com/blog/linear-webhook-tester/blog/receive-linear-webhooks-locally/) and build your handler. New to webhooks in general? Start with [what is a webhook](https://webhookrelay.com/blog/linear-webhook-tester/blog/what-is-webhook/) and [how to test webhooks](https://webhookrelay.com/blog/linear-webhook-tester/blog/how-to-test-webhooks/). Ready to inspect your first Linear event? [Open a free Webhook Bin](https://webhookrelay.com/blog/linear-webhook-tester/webhook-bin/) and paste the URL into Linear. --- --- title: Intercom Webhook Tester — Test & Inspect Online meta: "og: title": "Intercom Webhook Tester — Test & Inspect Online" description: Test and inspect Intercom webhooks online with a free webhook tester URL — capture real Intercom payloads, read the signature header, then forward locally. url: https://webhookrelay.com/blog/intercom-webhook-tester.md file: /blog/intercom-webhook-tester.md --- ![Stripes](https://webhookrelay.com/blog/intercom-webhook-tester/images/stripes.svg) # **Intercom Webhook Tester — Test & Inspect Online** Test and inspect Intercom webhooks online with a free webhook tester URL — capture real Intercom payloads, read the signature header, then forward locally. ![Intercom Webhook Tester](https://webhookrelay.com/blog/intercom-webhook-tester/images/blog/heroes/tester.jpg) If you are wiring up Intercom webhooks, the first question is always the same: _what does Intercom actually send?_ The docs show an idealised payload, but the real request — its headers, its `X-Hub-Signature` header, the exact JSON shape — is what your handler has to parse. A **Intercom webhook tester** gives you a public URL that captures those real requests so you can read every byte before you write any code. ## [Get a free Intercom webhook tester URL](#get-a-free-intercom-webhook-tester-url) The fastest way is our free [Webhook Bin](https://webhookrelay.com/blog/intercom-webhook-tester/webhook-bin/) — a no-code [webhook tester](https://webhookrelay.com/blog/intercom-webhook-tester/webhook-bin/) that gives you an instant public URL and stores every request that hits it, headers and body included. No signup, no deploy: 1. Open the [Webhook Bin](https://webhookrelay.com/blog/intercom-webhook-tester/webhook-bin/) and copy the URL it generates for you. 2. In **the Developer Hub → your app → Webhooks**, add a webhook endpoint and paste that URL. 3. Trigger an event (see below) and watch the request land in the bin in real time. Because the bin keeps the full request, you can inspect the `X-Hub-Signature` header, the `Content-Type`, and the complete payload — the three things you need to build and verify a handler. ## [What a Intercom webhook looks like](#what-a-intercom-webhook-looks-like) Intercom delivers webhooks as an HTTP POST with a `application/json` body. Intercom wraps every event in a `notification_event` envelope with a `topic` field, and signs with the older `X-Hub-Signature` (SHA-1) scheme — capture one to see the deeply nested `data.item` structure. A typical `conversation.user.created` payload looks like this: ``` { "type": "notification_event", "topic": "conversation.user.created", "data": { "item": { "type": "conversation", "id": "...", "source": { "author": { "type": "user", "email": "jon@example.com" } } } } } ``` Common Intercom events you will want to test: - `conversation.user.created` - `conversation.admin.replied` - `contact.created` ## [Verifying the Intercom signature](#verifying-the-intercom-signature) Intercom signs each request so you can prove it really came from Intercom. The signature travels in the **`X-Hub-Signature`** header and is HMAC-SHA1 of the raw request body, using your app's client secret. Capture a real request first, then use our [HMAC signature verifier](https://webhookrelay.com/blog/intercom-webhook-tester/hmac-verification/) and the [verify a webhook signature](https://webhookrelay.com/blog/intercom-webhook-tester/blog/verify-webhook-signature/) guide to confirm your verification logic against a payload you can actually see. ## [From inspecting to receiving on localhost](#from-inspecting-to-receiving-on-localhost) A bin is perfect for _seeing_ the payload. When you are ready to drive your **local** handler with real Intercom events — without deploying — forward them straight to `localhost` with the Webhook Relay agent. The full walkthrough is here: [Receive Intercom webhooks on localhost](https://webhookrelay.com/blog/intercom-webhook-tester/blog/receive-intercom-webhooks-locally/). That gives you a stable public URL that tunnels to your machine, so Intercom keeps delivering to the same endpoint while you iterate on `localhost`, no firewall changes or public IP required. ## [Test Intercom webhooks online in three steps](#test-intercom-webhooks-online-in-three-steps) 1. **Capture** — point Intercom at a [Webhook Bin](https://webhookrelay.com/blog/intercom-webhook-tester/webhook-bin/) URL and inspect the real request. 2. **Verify** — confirm the `X-Hub-Signature` header with the [HMAC verifier](https://webhookrelay.com/blog/intercom-webhook-tester/hmac-verification/). 3. **Forward** — when the shape is clear, [receive Intercom webhooks on localhost](https://webhookrelay.com/blog/intercom-webhook-tester/blog/receive-intercom-webhooks-locally/) and build your handler. New to webhooks in general? Start with [what is a webhook](https://webhookrelay.com/blog/intercom-webhook-tester/blog/what-is-webhook/) and [how to test webhooks](https://webhookrelay.com/blog/intercom-webhook-tester/blog/how-to-test-webhooks/). Ready to inspect your first Intercom event? [Open a free Webhook Bin](https://webhookrelay.com/blog/intercom-webhook-tester/webhook-bin/) and paste the URL into Intercom. --- --- title: Calendly Webhook Tester — Test & Inspect Online meta: "og: title": "Calendly Webhook Tester — Test & Inspect Online" description: Test and inspect Calendly webhooks online with a free webhook tester URL — capture real Calendly payloads, read the signature header, then forward locally. url: https://webhookrelay.com/blog/calendly-webhook-tester.md file: /blog/calendly-webhook-tester.md --- ![Stripes](https://webhookrelay.com/blog/calendly-webhook-tester/images/stripes.svg) # **Calendly Webhook Tester — Test & Inspect Online** Test and inspect Calendly webhooks online with a free webhook tester URL — capture real Calendly payloads, read the signature header, then forward locally. ![Calendly Webhook Tester](https://webhookrelay.com/blog/calendly-webhook-tester/images/blog/heroes/tester.jpg) If you are wiring up Calendly webhooks, the first question is always the same: _what does Calendly actually send?_ The docs show an idealised payload, but the real request — its headers, its `Calendly-Webhook-Signature` header, the exact JSON shape — is what your handler has to parse. A **Calendly webhook tester** gives you a public URL that captures those real requests so you can read every byte before you write any code. ## [Get a free Calendly webhook tester URL](#get-a-free-calendly-webhook-tester-url) The fastest way is our free [Webhook Bin](https://webhookrelay.com/blog/calendly-webhook-tester/webhook-bin/) — a no-code [webhook tester](https://webhookrelay.com/blog/calendly-webhook-tester/webhook-bin/) that gives you an instant public URL and stores every request that hits it, headers and body included. No signup, no deploy: 1. Open the [Webhook Bin](https://webhookrelay.com/blog/calendly-webhook-tester/webhook-bin/) and copy the URL it generates for you. 2. In **Integrations → Webhooks (or via the Webhook Subscriptions API)**, add a webhook endpoint and paste that URL. 3. Trigger an event (see below) and watch the request land in the bin in real time. Because the bin keeps the full request, you can inspect the `Calendly-Webhook-Signature` header, the `Content-Type`, and the complete payload — the three things you need to build and verify a handler. ## [What a Calendly webhook looks like](#what-a-calendly-webhook-looks-like) Calendly delivers webhooks as an HTTP POST with a `application/json` body. Calendly signs `timestamp.body` and returns it in `Calendly-Webhook-Signature` — capture a real `invitee.created` to see the `payload.scheduled_event` details your booking flow needs. A typical `invitee.created` payload looks like this: ``` { "event": "invitee.created", "created_at": "2026-06-11T15:00:00Z", "payload": { "email": "jon@example.com", "name": "Jon", "scheduled_event": { "name": "30 Minute Meeting", "start_time": "2026-06-12T16:00:00Z" } } } ``` Common Calendly events you will want to test: - `invitee.created` - `invitee.canceled` ## [Verifying the Calendly signature](#verifying-the-calendly-signature) Calendly signs each request so you can prove it really came from Calendly. The signature travels in the **`Calendly-Webhook-Signature`** header and is HMAC-SHA256 over `timestamp.body`, using the signing key returned when you create the subscription. Capture a real request first, then use our [HMAC signature verifier](https://webhookrelay.com/blog/calendly-webhook-tester/hmac-verification/) and the [verify a webhook signature](https://webhookrelay.com/blog/calendly-webhook-tester/blog/verify-webhook-signature/) guide to confirm your verification logic against a payload you can actually see. ## [From inspecting to receiving on localhost](#from-inspecting-to-receiving-on-localhost) A bin is perfect for _seeing_ the payload. When you are ready to drive your **local** handler with real Calendly events — without deploying — forward them straight to `localhost` with the Webhook Relay agent. The full walkthrough is here: [Receive Calendly webhooks on localhost](https://webhookrelay.com/blog/calendly-webhook-tester/blog/receive-calendly-webhooks-locally/). That gives you a stable public URL that tunnels to your machine, so Calendly keeps delivering to the same endpoint while you iterate on `localhost`, no firewall changes or public IP required. ## [Test Calendly webhooks online in three steps](#test-calendly-webhooks-online-in-three-steps) 1. **Capture** — point Calendly at a [Webhook Bin](https://webhookrelay.com/blog/calendly-webhook-tester/webhook-bin/) URL and inspect the real request. 2. **Verify** — confirm the `Calendly-Webhook-Signature` header with the [HMAC verifier](https://webhookrelay.com/blog/calendly-webhook-tester/hmac-verification/). 3. **Forward** — when the shape is clear, [receive Calendly webhooks on localhost](https://webhookrelay.com/blog/calendly-webhook-tester/blog/receive-calendly-webhooks-locally/) and build your handler. New to webhooks in general? Start with [what is a webhook](https://webhookrelay.com/blog/calendly-webhook-tester/blog/what-is-webhook/) and [how to test webhooks](https://webhookrelay.com/blog/calendly-webhook-tester/blog/how-to-test-webhooks/). Ready to inspect your first Calendly event? [Open a free Webhook Bin](https://webhookrelay.com/blog/calendly-webhook-tester/webhook-bin/) and paste the URL into Calendly. --- --- title: Typeform Webhook Tester — Test & Inspect Online meta: "og: title": "Typeform Webhook Tester — Test & Inspect Online" description: Test and inspect Typeform webhooks online with a free webhook tester URL — capture real Typeform payloads, read the signature header, then forward locally. url: https://webhookrelay.com/blog/typeform-webhook-tester.md file: /blog/typeform-webhook-tester.md --- ![Stripes](https://webhookrelay.com/blog/typeform-webhook-tester/images/stripes.svg) # **Typeform Webhook Tester — Test & Inspect Online** Test and inspect Typeform webhooks online with a free webhook tester URL — capture real Typeform payloads, read the signature header, then forward locally. ![Typeform Webhook Tester](https://webhookrelay.com/blog/typeform-webhook-tester/images/blog/heroes/tester.jpg) If you are wiring up Typeform webhooks, the first question is always the same: _what does Typeform actually send?_ The docs show an idealised payload, but the real request — its headers, its `Typeform-Signature` header, the exact JSON shape — is what your handler has to parse. A **Typeform webhook tester** gives you a public URL that captures those real requests so you can read every byte before you write any code. ## [Get a free Typeform webhook tester URL](#get-a-free-typeform-webhook-tester-url) The fastest way is our free [Webhook Bin](https://webhookrelay.com/blog/typeform-webhook-tester/webhook-bin/) — a no-code [webhook tester](https://webhookrelay.com/blog/typeform-webhook-tester/webhook-bin/) that gives you an instant public URL and stores every request that hits it, headers and body included. No signup, no deploy: 1. Open the [Webhook Bin](https://webhookrelay.com/blog/typeform-webhook-tester/webhook-bin/) and copy the URL it generates for you. 2. In **a form's Connect → Webhooks panel (or the Webhooks API)**, add a webhook endpoint and paste that URL. 3. Trigger an event (see below) and watch the request land in the bin in real time. Because the bin keeps the full request, you can inspect the `Typeform-Signature` header, the `Content-Type`, and the complete payload — the three things you need to build and verify a handler. ## [What a Typeform webhook looks like](#what-a-typeform-webhook-looks-like) Typeform delivers webhooks as an HTTP POST with a `application/json` body. Typeform fires a single `form_response` event per submission and signs the body, returning the digest in `Typeform-Signature` as `sha256=...` — capture one to map each answer's `field.ref` to your form's questions. A typical `form_response` payload looks like this: ``` { "event_id": "...", "event_type": "form_response", "form_response": { "form_id": "...", "token": "...", "answers": [ { "type": "text", "text": "Jon", "field": { "ref": "name" } } ] } } ``` Common Typeform events you will want to test: - `form_response` ## [Verifying the Typeform signature](#verifying-the-typeform-signature) Typeform signs each request so you can prove it really came from Typeform. The signature travels in the **`Typeform-Signature`** header and is base64 HMAC-SHA256 of the raw body, prefixed with `sha256=`, using the secret you set when creating the webhook. Capture a real request first, then use our [HMAC signature verifier](https://webhookrelay.com/blog/typeform-webhook-tester/hmac-verification/) and the [verify a webhook signature](https://webhookrelay.com/blog/typeform-webhook-tester/blog/verify-webhook-signature/) guide to confirm your verification logic against a payload you can actually see. ## [From inspecting to receiving on localhost](#from-inspecting-to-receiving-on-localhost) A bin is perfect for _seeing_ the payload. When you are ready to drive your **local** handler with real Typeform events — without deploying — forward them straight to `localhost` with the Webhook Relay agent. The full walkthrough is here: [Receive Typeform webhooks on localhost](https://webhookrelay.com/blog/typeform-webhook-tester/blog/receive-typeform-webhooks-locally/). That gives you a stable public URL that tunnels to your machine, so Typeform keeps delivering to the same endpoint while you iterate on `localhost`, no firewall changes or public IP required. ## [Test Typeform webhooks online in three steps](#test-typeform-webhooks-online-in-three-steps) 1. **Capture** — point Typeform at a [Webhook Bin](https://webhookrelay.com/blog/typeform-webhook-tester/webhook-bin/) URL and inspect the real request. 2. **Verify** — confirm the `Typeform-Signature` header with the [HMAC verifier](https://webhookrelay.com/blog/typeform-webhook-tester/hmac-verification/). 3. **Forward** — when the shape is clear, [receive Typeform webhooks on localhost](https://webhookrelay.com/blog/typeform-webhook-tester/blog/receive-typeform-webhooks-locally/) and build your handler. New to webhooks in general? Start with [what is a webhook](https://webhookrelay.com/blog/typeform-webhook-tester/blog/what-is-webhook/) and [how to test webhooks](https://webhookrelay.com/blog/typeform-webhook-tester/blog/how-to-test-webhooks/). Ready to inspect your first Typeform event? [Open a free Webhook Bin](https://webhookrelay.com/blog/typeform-webhook-tester/webhook-bin/) and paste the URL into Typeform. --- --- title: Razorpay Webhook Tester — Test & Inspect Online meta: "og: title": "Razorpay Webhook Tester — Test & Inspect Online" description: Test and inspect Razorpay webhooks online with a free webhook tester URL — capture real Razorpay payloads, read the signature header, then forward locally. url: https://webhookrelay.com/blog/razorpay-webhook-tester.md file: /blog/razorpay-webhook-tester.md --- ![Stripes](https://webhookrelay.com/blog/razorpay-webhook-tester/images/stripes.svg) # **Razorpay Webhook Tester — Test & Inspect Online** Test and inspect Razorpay webhooks online with a free webhook tester URL — capture real Razorpay payloads, read the signature header, then forward locally. ![Razorpay Webhook Tester](https://webhookrelay.com/blog/razorpay-webhook-tester/images/blog/heroes/tester.jpg) If you are wiring up Razorpay webhooks, the first question is always the same: _what does Razorpay actually send?_ The docs show an idealised payload, but the real request — its headers, its `X-Razorpay-Signature` header, the exact JSON shape — is what your handler has to parse. A **Razorpay webhook tester** gives you a public URL that captures those real requests so you can read every byte before you write any code. ## [Get a free Razorpay webhook tester URL](#get-a-free-razorpay-webhook-tester-url) The fastest way is our free [Webhook Bin](https://webhookrelay.com/blog/razorpay-webhook-tester/webhook-bin/) — a no-code [webhook tester](https://webhookrelay.com/blog/razorpay-webhook-tester/webhook-bin/) that gives you an instant public URL and stores every request that hits it, headers and body included. No signup, no deploy: 1. Open the [Webhook Bin](https://webhookrelay.com/blog/razorpay-webhook-tester/webhook-bin/) and copy the URL it generates for you. 2. In **Dashboard → Settings → Webhooks**, add a webhook endpoint and paste that URL. 3. Trigger an event (see below) and watch the request land in the bin in real time. Because the bin keeps the full request, you can inspect the `X-Razorpay-Signature` header, the `Content-Type`, and the complete payload — the three things you need to build and verify a handler. ## [What a Razorpay webhook looks like](#what-a-razorpay-webhook-looks-like) Razorpay delivers webhooks as an HTTP POST with a `application/json` body. Razorpay names the event in the body's `event` field and signs the raw payload in `X-Razorpay-Signature` — capture a real `payment.captured` to see the nested `payload.payment.entity` your handler reads. A typical `payment.captured` payload looks like this: ``` { "event": "payment.captured", "contains": [ "payment" ], "payload": { "payment": { "entity": { "id": "pay_...", "amount": 50000, "currency": "INR", "status": "captured" } } } } ``` Common Razorpay events you will want to test: - `payment.captured` - `payment.failed` - `order.paid` - `subscription.charged` ## [Verifying the Razorpay signature](#verifying-the-razorpay-signature) Razorpay signs each request so you can prove it really came from Razorpay. The signature travels in the **`X-Razorpay-Signature`** header and is HMAC-SHA256 of the raw request body, using the webhook secret you set in the dashboard. Capture a real request first, then use our [HMAC signature verifier](https://webhookrelay.com/blog/razorpay-webhook-tester/hmac-verification/) and the [verify a webhook signature](https://webhookrelay.com/blog/razorpay-webhook-tester/blog/verify-webhook-signature/) guide to confirm your verification logic against a payload you can actually see. ## [From inspecting to receiving on localhost](#from-inspecting-to-receiving-on-localhost) A bin is perfect for _seeing_ the payload. When you are ready to drive your **local** handler with real Razorpay events — without deploying — forward them straight to `localhost` with the Webhook Relay agent. The full walkthrough is here: [Receive Razorpay webhooks on localhost](https://webhookrelay.com/blog/razorpay-webhook-tester/blog/receive-razorpay-webhooks-locally/). That gives you a stable public URL that tunnels to your machine, so Razorpay keeps delivering to the same endpoint while you iterate on `localhost`, no firewall changes or public IP required. ## [Test Razorpay webhooks online in three steps](#test-razorpay-webhooks-online-in-three-steps) 1. **Capture** — point Razorpay at a [Webhook Bin](https://webhookrelay.com/blog/razorpay-webhook-tester/webhook-bin/) URL and inspect the real request. 2. **Verify** — confirm the `X-Razorpay-Signature` header with the [HMAC verifier](https://webhookrelay.com/blog/razorpay-webhook-tester/hmac-verification/). 3. **Forward** — when the shape is clear, [receive Razorpay webhooks on localhost](https://webhookrelay.com/blog/razorpay-webhook-tester/blog/receive-razorpay-webhooks-locally/) and build your handler. New to webhooks in general? Start with [what is a webhook](https://webhookrelay.com/blog/razorpay-webhook-tester/blog/what-is-webhook/) and [how to test webhooks](https://webhookrelay.com/blog/razorpay-webhook-tester/blog/how-to-test-webhooks/). Ready to inspect your first Razorpay event? [Open a free Webhook Bin](https://webhookrelay.com/blog/razorpay-webhook-tester/webhook-bin/) and paste the URL into Razorpay. --- --- title: Mailgun Webhook Tester — Test & Inspect Online meta: "og: title": "Mailgun Webhook Tester — Test & Inspect Online" description: Test and inspect Mailgun webhooks online with a free webhook tester URL — capture real Mailgun payloads, see what arrives, then forward locally. url: https://webhookrelay.com/blog/mailgun-webhook-tester.md file: /blog/mailgun-webhook-tester.md --- ![Stripes](https://webhookrelay.com/blog/mailgun-webhook-tester/images/stripes.svg) # **Mailgun Webhook Tester — Test & Inspect Online** Test and inspect Mailgun webhooks online with a free webhook tester URL — capture real Mailgun payloads, see what arrives, then forward locally. ![Mailgun Webhook Tester](https://webhookrelay.com/blog/mailgun-webhook-tester/images/blog/heroes/tester.jpg) If you are wiring up Mailgun webhooks, the first question is always the same: _what does Mailgun actually send?_ The docs show an idealised payload, but the real request — its headers and the exact JSON shape — is what your handler has to parse. A **Mailgun webhook tester** gives you a public URL that captures those real requests so you can read every byte before you write any code. ## [Get a free Mailgun webhook tester URL](#get-a-free-mailgun-webhook-tester-url) The fastest way is our free [Webhook Bin](https://webhookrelay.com/blog/mailgun-webhook-tester/webhook-bin/) — a no-code [webhook tester](https://webhookrelay.com/blog/mailgun-webhook-tester/webhook-bin/) that gives you an instant public URL and stores every request that hits it, headers and body included. No signup, no deploy: 1. Open the [Webhook Bin](https://webhookrelay.com/blog/mailgun-webhook-tester/webhook-bin/) and copy the URL it generates for you. 2. In **the Mailgun dashboard → **Sending → Webhooks****, add a webhook endpoint and paste that URL. 3. Trigger an event (see below) and watch the request land in the bin in real time. Because the bin keeps the full request, you can inspect the headers, the `Content-Type`, and the complete payload — everything you need to build and verify a handler. ## [What a Mailgun webhook looks like](#what-a-mailgun-webhook-looks-like) Mailgun delivers webhooks as an HTTP POST with a `application/json` body. Mailgun's modern webhooks POST JSON with two top-level objects: a `signature` object (`timestamp`, `token`, `signature`) and an `event-data` object describing what happened. Note that the signature lives **in the body**, not in a header. A typical payload looks like this: ``` { "signature": { "timestamp": "1700000000", "token": "abc123...", "signature": "" }, "event-data": { "event": "delivered", "recipient": "jon@example.com", "message": { "headers": { "message-id": "..." } } } } ``` Common Mailgun events you will want to test: - `delivered` - `opened` - `clicked` - `permanent_fail (bounced)` - `complained` - `unsubscribed` ## [Verifying the Mailgun signature](#verifying-the-mailgun-signature) Mailgun does **not** put its signature in a header. Each webhook body includes a `signature` object with three fields: `timestamp`, `token` and `signature`. To verify, concatenate `timestamp` + `token`, compute an **HMAC-SHA256** of that string keyed by your **Mailgun HTTP webhook signing key** (Settings → Webhooks), hex-encode it, and compare it to the `signature` field in constant time. Also reject requests whose `timestamp` is not recent so a captured `token` can't be replayed. You can sanity-check an HMAC implementation with the free [HMAC signature verifier](https://webhookrelay.com/blog/mailgun-webhook-tester/hmac-verification/); for language-specific code and the common pitfalls, read [verify a webhook signature](https://webhookrelay.com/blog/mailgun-webhook-tester/blog/verify-webhook-signature/). ## [From inspecting to receiving on localhost](#from-inspecting-to-receiving-on-localhost) A bin is perfect for _seeing_ the payload. When you are ready to drive your **local** handler with real Mailgun events — without deploying — forward them straight to `localhost` with the Webhook Relay agent. The full walkthrough is here: [Receive Mailgun webhooks on localhost](https://webhookrelay.com/blog/mailgun-webhook-tester/blog/receive-mailgun-webhooks-locally/). That gives you a stable public URL that tunnels to your machine, so Mailgun keeps delivering to the same endpoint while you iterate on `localhost`, no firewall changes or public IP required. ## [Test Mailgun webhooks online in three steps](#test-mailgun-webhooks-online-in-three-steps) 1. **Capture** — point Mailgun at a [Webhook Bin](https://webhookrelay.com/blog/mailgun-webhook-tester/webhook-bin/) URL and inspect the real request. 2. **Verify** — confirm the request is authentic (see above). 3. **Forward** — when the shape is clear, [receive Mailgun webhooks on localhost](https://webhookrelay.com/blog/mailgun-webhook-tester/blog/receive-mailgun-webhooks-locally/) and build your handler. New to webhooks in general? Start with [what is a webhook](https://webhookrelay.com/blog/mailgun-webhook-tester/blog/what-is-webhook/) and [how to test webhooks](https://webhookrelay.com/blog/mailgun-webhook-tester/blog/how-to-test-webhooks/). Ready to inspect your first Mailgun event? [Open a free Webhook Bin](https://webhookrelay.com/blog/mailgun-webhook-tester/webhook-bin/) and paste the URL into Mailgun. --- --- title: SendGrid Webhook Tester — Test & Inspect Online meta: "og: title": "SendGrid Webhook Tester — Test & Inspect Online" description: Test and inspect SendGrid webhooks online with a free webhook tester URL — capture real SendGrid payloads, see what arrives, then forward locally. url: https://webhookrelay.com/blog/sendgrid-webhook-tester.md file: /blog/sendgrid-webhook-tester.md --- ![Stripes](https://webhookrelay.com/blog/sendgrid-webhook-tester/images/stripes.svg) # **SendGrid Webhook Tester — Test & Inspect Online** Test and inspect SendGrid webhooks online with a free webhook tester URL — capture real SendGrid payloads, see what arrives, then forward locally. ![SendGrid Webhook Tester](https://webhookrelay.com/blog/sendgrid-webhook-tester/images/blog/heroes/tester.jpg) If you are wiring up SendGrid webhooks, the first question is always the same: _what does SendGrid actually send?_ The docs show an idealised payload, but the real request — its headers and the exact JSON shape — is what your handler has to parse. A **SendGrid webhook tester** gives you a public URL that captures those real requests so you can read every byte before you write any code. ## [Get a free SendGrid webhook tester URL](#get-a-free-sendgrid-webhook-tester-url) The fastest way is our free [Webhook Bin](https://webhookrelay.com/blog/sendgrid-webhook-tester/webhook-bin/) — a no-code [webhook tester](https://webhookrelay.com/blog/sendgrid-webhook-tester/webhook-bin/) that gives you an instant public URL and stores every request that hits it, headers and body included. No signup, no deploy: 1. Open the [Webhook Bin](https://webhookrelay.com/blog/sendgrid-webhook-tester/webhook-bin/) and copy the URL it generates for you. 2. In **SendGrid → **Settings → Mail Settings → Event Webhook****, add a webhook endpoint and paste that URL. 3. Trigger an event (see below) and watch the request land in the bin in real time. Because the bin keeps the full request, you can inspect the headers, the `Content-Type`, and the complete payload — everything you need to build and verify a handler. ## [What a SendGrid webhook looks like](#what-a-sendgrid-webhook-looks-like) SendGrid delivers webhooks as an HTTP POST with a `application/json` body. SendGrid batches events into a JSON **array** — one POST can carry many events. When you enable the **Signed Event Webhook**, each request also carries `X-Twilio-Email-Event-Webhook-Signature` and `X-Twilio-Email-Event-Webhook-Timestamp` headers. A typical payload looks like this: ``` [ { "email": "jon@example.com", "event": "delivered", "sg_event_id": "...", "sg_message_id": "...", "timestamp": 1700000000 } ] ``` Common SendGrid events you will want to test: - `delivered` - `open` - `click` - `bounce` - `dropped` - `spamreport` - `unsubscribe` ## [Verifying the SendGrid signature](#verifying-the-sendgrid-signature) SendGrid's Event Webhook uses **ECDSA**, not HMAC. Turn on the **Signed Event Webhook** to get a **verification key** (an ECDSA public key). SendGrid signs the concatenation of the `X-Twilio-Email-Event-Webhook-Timestamp` header and the raw payload, and sends the base64 signature in the `X-Twilio-Email-Event-Webhook-Signature` header. To verify, rebuild `timestamp + rawBody` and check the ECDSA signature against your public verification key. Capturing a real request first lets you confirm both headers and the exact raw body the signature was computed over. You can sanity-check an HMAC implementation with the free [HMAC signature verifier](https://webhookrelay.com/blog/sendgrid-webhook-tester/hmac-verification/); for language-specific code and the common pitfalls, read [verify a webhook signature](https://webhookrelay.com/blog/sendgrid-webhook-tester/blog/verify-webhook-signature/). ## [From inspecting to receiving on localhost](#from-inspecting-to-receiving-on-localhost) A bin is perfect for _seeing_ the payload. When you are ready to drive your **local** handler with real SendGrid events — without deploying — forward them straight to `localhost` with the Webhook Relay agent. The full walkthrough is here: [Receive SendGrid webhooks on localhost](https://webhookrelay.com/blog/sendgrid-webhook-tester/blog/receive-sendgrid-webhooks-locally/). That gives you a stable public URL that tunnels to your machine, so SendGrid keeps delivering to the same endpoint while you iterate on `localhost`, no firewall changes or public IP required. ## [Test SendGrid webhooks online in three steps](#test-sendgrid-webhooks-online-in-three-steps) 1. **Capture** — point SendGrid at a [Webhook Bin](https://webhookrelay.com/blog/sendgrid-webhook-tester/webhook-bin/) URL and inspect the real request. 2. **Verify** — confirm the request is authentic (see above). 3. **Forward** — when the shape is clear, [receive SendGrid webhooks on localhost](https://webhookrelay.com/blog/sendgrid-webhook-tester/blog/receive-sendgrid-webhooks-locally/) and build your handler. New to webhooks in general? Start with [what is a webhook](https://webhookrelay.com/blog/sendgrid-webhook-tester/blog/what-is-webhook/) and [how to test webhooks](https://webhookrelay.com/blog/sendgrid-webhook-tester/blog/how-to-test-webhooks/). Ready to inspect your first SendGrid event? [Open a free Webhook Bin](https://webhookrelay.com/blog/sendgrid-webhook-tester/webhook-bin/) and paste the URL into SendGrid. --- --- title: Datadog Webhook Tester — Test & Inspect Online meta: "og: title": "Datadog Webhook Tester — Test & Inspect Online" description: Test and inspect Datadog webhooks online with a free webhook tester URL — capture real Datadog payloads, see what arrives, then forward locally. url: https://webhookrelay.com/blog/datadog-webhook-tester.md file: /blog/datadog-webhook-tester.md --- ![Stripes](https://webhookrelay.com/blog/datadog-webhook-tester/images/stripes.svg) # **Datadog Webhook Tester — Test & Inspect Online** Test and inspect Datadog webhooks online with a free webhook tester URL — capture real Datadog payloads, see what arrives, then forward locally. ![Datadog Webhook Tester](https://webhookrelay.com/blog/datadog-webhook-tester/images/blog/heroes/tester.jpg) If you are wiring up Datadog webhooks, the first question is always the same: _what does Datadog actually send?_ The docs show an idealised payload, but the real request — its headers and the exact JSON shape — is what your handler has to parse. A **Datadog webhook tester** gives you a public URL that captures those real requests so you can read every byte before you write any code. ## [Get a free Datadog webhook tester URL](#get-a-free-datadog-webhook-tester-url) The fastest way is our free [Webhook Bin](https://webhookrelay.com/blog/datadog-webhook-tester/webhook-bin/) — a no-code [webhook tester](https://webhookrelay.com/blog/datadog-webhook-tester/webhook-bin/) that gives you an instant public URL and stores every request that hits it, headers and body included. No signup, no deploy: 1. Open the [Webhook Bin](https://webhookrelay.com/blog/datadog-webhook-tester/webhook-bin/) and copy the URL it generates for you. 2. In **Datadog → **Integrations → Webhooks****, add a webhook endpoint and paste that URL. 3. Trigger an event (see below) and watch the request land in the bin in real time. Because the bin keeps the full request, you can inspect the headers, the `Content-Type`, and the complete payload — everything you need to build and verify a handler. ## [What a Datadog webhook looks like](#what-a-datadog-webhook-looks-like) Datadog delivers webhooks as an HTTP POST with a `application/json` body. Datadog webhooks are **custom**: you define the JSON payload yourself in the webhook's **Payload** field using template variables like `$EVENT_TITLE`, `$EVENT_MSG` and `$ALERT_TYPE`. So the shape is whatever you template — capturing a real fire shows you exactly what your template renders to. A typical payload looks like this: ``` { "title": "[Triggered] CPU high on web-1", "body": "CPU is above 90%", "alert_type": "error", "event_id": "..." } ``` Common Datadog events you will want to test: - `monitor alert` - `monitor recovery` - `monitor warning` - `any event you route to the webhook` ## [Securing the Datadog webhook](#securing-the-datadog-webhook) Datadog webhooks have **no built-in cryptographic signature**. Instead, you secure them with a shared secret you control: in the webhook configuration add a **custom header** (for example `X-Webhook-Secret: `) under **Headers**, then reject any request to your endpoint that doesn't carry it. Because you also control the **payload template**, you can include a secret field in the body as well. Capturing a real request in a tester confirms your custom header and templated payload arrive exactly as configured. You can sanity-check an HMAC implementation with the free [HMAC signature verifier](https://webhookrelay.com/blog/datadog-webhook-tester/hmac-verification/); for language-specific code and the common pitfalls, read [verify a webhook signature](https://webhookrelay.com/blog/datadog-webhook-tester/blog/verify-webhook-signature/). ## [From inspecting to receiving on localhost](#from-inspecting-to-receiving-on-localhost) A bin is perfect for _seeing_ the payload. When you are ready to drive your **local** handler with real Datadog events — without deploying — forward them straight to `localhost` with the Webhook Relay agent. The full walkthrough is here: [Receive Datadog webhooks on localhost](https://webhookrelay.com/blog/datadog-webhook-tester/blog/receive-datadog-webhooks-locally/). That gives you a stable public URL that tunnels to your machine, so Datadog keeps delivering to the same endpoint while you iterate on `localhost`, no firewall changes or public IP required. ## [Test Datadog webhooks online in three steps](#test-datadog-webhooks-online-in-three-steps) 1. **Capture** — point Datadog at a [Webhook Bin](https://webhookrelay.com/blog/datadog-webhook-tester/webhook-bin/) URL and inspect the real request. 2. **Verify** — confirm the request is authentic (see above). 3. **Forward** — when the shape is clear, [receive Datadog webhooks on localhost](https://webhookrelay.com/blog/datadog-webhook-tester/blog/receive-datadog-webhooks-locally/) and build your handler. New to webhooks in general? Start with [what is a webhook](https://webhookrelay.com/blog/datadog-webhook-tester/blog/what-is-webhook/) and [how to test webhooks](https://webhookrelay.com/blog/datadog-webhook-tester/blog/how-to-test-webhooks/). Ready to inspect your first Datadog event? [Open a free Webhook Bin](https://webhookrelay.com/blog/datadog-webhook-tester/webhook-bin/) and paste the URL into Datadog. --- --- title: Send a Webhook to Discord | WebhookRelay meta: "og: title": "Send a Webhook to Discord" description: Send any webhook to Discord. Forward an incoming webhook to a Discord channel, transforming the raw payload into Discord's content format in flight. url: https://webhookrelay.com/blog/webhook-to-discord.md file: /blog/webhook-to-discord.md --- ![Stripes](https://webhookrelay.com/blog/webhook-to-discord/images/stripes.svg) # **Send a Webhook to Discord** Send any webhook to Discord. Forward an incoming webhook to a Discord channel, transforming the raw payload into Discord's content format in flight. ![Send a Webhook to Discord: Transform & Forward Any Payload](https://webhookrelay.com/blog/webhook-to-discord/images/blog/heroes/route.jpg) You have a service that fires webhooks — a CI pipeline, a contact form, an uptime monitor, a payment provider, an alerting tool — and you want each event to show up in a **Discord channel**. The problem: Discord doesn't accept arbitrary payloads. It expects a specific JSON body, and the webhook your source sends almost never matches that shape. [Webhook Relay](https://webhookrelay.com) sits in the middle. It receives the incoming webhook at a stable public URL, **transforms** the payload into the format Discord expects, and delivers it — no glue server, no Lambda, no maintenance. (Don't have one yet? See [how to get a Discord webhook URL](https://webhookrelay.com/blog/webhook-to-discord/blog/how-to-get-discord-webhook-url/).) > **Build it visually first:** the free [webhook → Slack/Discord/Teams formatter](https://webhookrelay.com/blog/webhook-to-discord/webhook-message-formatter/) lets you paste a sample payload, template the Discord embed, preview it live, and copy the exact transform function used below — a faster, automatable [Discohook alternative](https://webhookrelay.com/blog/webhook-to-discord/webhook-message-formatter/) for incoming webhooks. ## [How it works](#how-it-works) The flow is a single hop with a transform in the middle: ``` Your service ──▶ Webhook Relay ──▶ transform to { content } ──▶ Discord channel (raw webhook) (public URL) (serverless function) (message appears) ``` Discord expects a JSON body like this POSTed to a Discord webhook URL: ``` { "content": "your message" } ``` A generic incoming webhook isn't shaped like that, so we add a small [transformation function](https://webhookrelay.com/blog/webhook-to-discord/features/transform-webhooks/) that wraps the raw body into Discord's format before delivery. That one feature is what turns Webhook Relay into a universal "anything → Discord" bridge. ## [Step 1: Create the Discord webhook URL](#step-1-create-the-discord-webhook-url) In Discord, open **Server Settings → Integrations → Webhooks → New Webhook**. Pick the channel you want messages to land in, give it a name, and click **Copy Webhook URL**. It looks like: ``` https://discord.com/api/webhooks/123456789/AbCdEf... ``` Keep this URL handy — it's the destination Webhook Relay will deliver to. ## [Step 2: Create a Webhook Relay public endpoint](#step-2-create-a-webhook-relay-public-endpoint) Open [the new public destination page](https://my.webhookrelay.com/new-public-destination) and paste the Discord webhook URL as the **destination**. Webhook Relay gives you back an **input URL** — a public endpoint that you'll point your source service at. That input URL is now a stable address you can hand to any service. Everything sent to it gets forwarded toward Discord. ## [Step 3: Add a transformation function](#step-3-add-a-transformation-function) Right now, anything you send to the input URL would be forwarded to Discord _as-is_ — and Discord would reject it, because it isn't in `{ "content": "..." }` shape. Add a [transformation](https://webhookrelay.com/blog/webhook-to-discord/features/transform-webhooks/) to fix that. Click **Transform** on the input, open the [Functions](https://my.webhookrelay.com/functions) page, create a function "from scratch", and paste this Lua: ``` local json = require("json") local payload = { content = r.RequestBody, } local body, err = json.encode(payload) if err then error(err) end r:SetRequestHeader("content-type", "application/json") r:SetRequestMethod("POST") r:SetRequestBody(body) ``` This takes the raw incoming body, wraps it as `{ "content": "..." }`, sets the right headers and method, and lets Webhook Relay deliver it. Discord now accepts the message and posts it to your channel. ## [Step 4: Point your source at the URL and test](#step-4-point-your-source-at-the-url-and-test) Configure your source service (the CI tool, form, monitor, payment provider — whatever you're integrating) to send its webhook to the Webhook Relay **input URL**. To confirm everything works before wiring up the real service, fire a quick test with `curl`: ``` curl -X POST https://my.webhookrelay.com/v1/webhooks/your-input-id \ -d 'Deployment finished: build #482 passed on main' ``` Within a second or two, **Deployment finished: build #482 passed on main** appears in your Discord channel. If nothing shows up, open the request log in your dashboard to see exactly what was received and delivered. ## [Going further](#going-further) Once the basic bridge works, the transform function is where the real power is. **Build a richer message with embeds.** If your source sends JSON, parse it and construct a Discord `embeds` object instead of a flat string — title, description, a colored sidebar, and labelled fields: ``` local json = require("json") local event = json.decode(r.RequestBody) local payload = { embeds = { { title = "Deployment " .. event.status, description = event.message, color = 5814783, }, }, } local body, err = json.encode(payload) if err then error(err) end r:SetRequestHeader("content-type", "application/json") r:SetRequestMethod("POST") r:SetRequestBody(body) ``` **Filter out noise.** Not every event deserves a ping. Use [forwarding rules](https://webhookrelay.com/blog/webhook-to-discord/features/forwarding-rules/) to only deliver events that match a condition, or drop them in the function with an early `return`. **Fan out to multiple channels.** Add more destinations so one incoming webhook lands in several Discord channels — or in Discord _and_ Slack — at the same time. See [forwarding to multiple destinations](https://webhookrelay.com/blog/webhook-to-discord/features/webhook-multiple-destinations/). For a complete real-world walkthrough using this exact pattern, see [TradingView alerts to Discord](https://webhookrelay.com/blog/webhook-to-discord/blog/trading-view/), where a plain alert message is transformed into a Discord notification. ## [FAQ](#faq) **What does Discord need to receive?** A `POST` with a JSON body of `{ "content": "your message" }` (or an `embeds` array for rich cards) to a Discord channel webhook URL. The transform function produces exactly that. **Do I need to run a server?** No. Webhook Relay receives, transforms, and delivers everything in the cloud. There's nothing to host or keep online. **How do I see the raw payload my source sends?** Send it to a free [Webhook Bin](https://webhookrelay.com/blog/webhook-to-discord/webhook-bin/) first to inspect the exact body and headers, then write your transform around that structure. ## [Get started](#get-started) [Create a free Webhook Relay account](https://my.webhookrelay.com/register) and set up your first "anything → Discord" bridge in a few minutes. Not sure what your source sends? [Inspect the payload in a Webhook Bin](https://webhookrelay.com/blog/webhook-to-discord/webhook-bin/) first, then write the transform to match. --- --- title: Send a Webhook to Microsoft Teams | WebhookRelay meta: "og: title": "Send a Webhook to Microsoft Teams" description: Send any incoming webhook to Microsoft Teams. Transform the raw payload into Teams message JSON in flight and post it to an Incoming Webhook URL. url: https://webhookrelay.com/blog/webhook-to-microsoft-teams.md file: /blog/webhook-to-microsoft-teams.md --- ![Stripes](https://webhookrelay.com/blog/webhook-to-microsoft-teams/images/stripes.svg) # **Send a Webhook to Microsoft Teams** Send any incoming webhook to Microsoft Teams. Transform the raw payload into Teams message JSON in flight and post it to an Incoming Webhook URL. ![Send a Webhook to Microsoft Teams: Transform & Forward Any Payload](https://webhookrelay.com/blog/webhook-to-microsoft-teams/images/blog/heroes/route.jpg) You have a service that fires webhooks — a CI pipeline, an uptime monitor, a form, a payment provider, an alerting tool — and you want each event to land in a **Microsoft Teams channel**. The problem: Teams doesn't accept arbitrary payloads. It expects a specific JSON body, and the webhook your source sends almost never matches that shape. [Webhook Relay](https://webhookrelay.com) sits in the middle. It receives the incoming webhook at a stable public URL, **transforms** the payload into the format Teams expects, and delivers it — no glue server, no Lambda, no maintenance. (Don't have one yet? See [how to create a Microsoft Teams webhook](https://webhookrelay.com/blog/webhook-to-microsoft-teams/blog/microsoft-teams-webhook/).) > **Build it visually first:** the free [webhook → Slack/Discord/Teams formatter](https://webhookrelay.com/blog/webhook-to-microsoft-teams/webhook-message-formatter/) lets you paste a sample payload, template the Teams message, preview it live, and copy the exact transform function used below — for any incoming webhook. ## [How it works](#how-it-works) The flow is a single hop with a transform in the middle: ``` Your service ──▶ Webhook Relay ──▶ transform to Teams JSON ──▶ Teams channel (raw webhook) (public URL) (serverless function) (message appears) ``` The simplest message Teams accepts is a JSON body with a `text` field POSTed to a Teams Incoming Webhook URL: ``` { "text": "your message" } ``` A generic incoming webhook isn't shaped like that, so we add a small [transformation function](https://webhookrelay.com/blog/webhook-to-microsoft-teams/features/transform-webhooks/) that wraps the raw body into Teams' format before delivery. That one feature is what turns Webhook Relay into a universal "anything → Teams" bridge. ## [A note on connectors vs. Workflows](#a-note-on-connectors-vs-workflows) Historically, Teams Incoming Webhooks came from **Office 365 connectors** and accepted the legacy **MessageCard** format. Microsoft is now **migrating away from connectors toward Power Automate Workflows**, where the modern format is an **Adaptive Card**. New integrations should create a **Workflow-based Incoming Webhook**. Good news: the Webhook Relay side doesn't change. Whichever URL you're given, your job is the same — produce the JSON that endpoint expects and POST it. The `{ "text": "..." }` shape below is the quickest to get working; we'll show the richer Adaptive Card variant at the end. ## [Step 1: Create the Teams webhook URL](#step-1-create-the-teams-webhook-url) In Teams, the current path is **+ New (or the channel's ••• menu) → Workflows → "Post to a channel when a webhook request is received"** (a Power Automate template). Select the team and channel, finish the flow, and copy the generated **HTTP POST URL**. It's a long Power Automate / Logic Apps URL. > If your tenant still uses the legacy route, you'd add an **Incoming Webhook** connector to the channel instead and copy that URL. Either URL works as a destination below. Keep this URL handy — it's where Webhook Relay will deliver. ## [Step 2: Create a Webhook Relay public endpoint](#step-2-create-a-webhook-relay-public-endpoint) Open [the new public destination page](https://my.webhookrelay.com/new-public-destination) and paste the Teams webhook URL as the **destination**. Webhook Relay gives you back an **input URL** — a public endpoint you'll point your source service at. That input URL is a stable address you can hand to any service. Everything sent to it gets forwarded toward Teams. ## [Step 3: Add a transformation function](#step-3-add-a-transformation-function) Right now, anything you send to the input URL would be forwarded to Teams _as-is_ — and Teams would reject it, because it isn't in the shape it expects. Add a [transformation](https://webhookrelay.com/blog/webhook-to-microsoft-teams/features/transform-webhooks/) to fix that. Click **Transform** on the input, open the [Functions](https://my.webhookrelay.com/functions) page, create a function "from scratch", and paste this Lua: ``` local json = require("json") local payload = { text = r.RequestBody, } local body, err = json.encode(payload) if err then error(err) end r:SetRequestHeader("content-type", "application/json") r:SetRequestMethod("POST") r:SetRequestBody(body) ``` This takes the raw incoming body, wraps it as `{ "text": "..." }`, sets the right headers and method, and lets Webhook Relay deliver it. Teams now accepts the message and posts it to your channel. ## [Step 4: Point your source at the URL and test](#step-4-point-your-source-at-the-url-and-test) Configure your source service (the CI tool, monitor, form, payment provider — whatever you're integrating) to send its webhook to the Webhook Relay **input URL**. To confirm everything works before wiring up the real service, fire a quick test with `curl`: ``` curl -X POST https://my.webhookrelay.com/v1/webhooks/your-input-id \ -d 'Deployment finished: build #482 passed on main' ``` Within a second or two, **Deployment finished: build #482 passed on main** appears in your Teams channel. If nothing shows up, open the request log in your dashboard to see exactly what was received and delivered. ## [Going further](#going-further) Once the basic bridge works, the transform function is where the real power is. **Build a richer Adaptive Card.** Workflow-based webhooks expect an Adaptive Card wrapped in an `attachments` array. If your source sends JSON, parse it and construct the card so you get titled, formatted messages instead of a flat string: ``` local json = require("json") local event = json.decode(r.RequestBody) local payload = { type = "message", attachments = { { contentType = "application/vnd.microsoft.card.adaptive", content = { ["$schema"] = "http://adaptivecards.io/schemas/adaptive-card.json", type = "AdaptiveCard", version = "1.4", body = { { type = "TextBlock", size = "Large", weight = "Bolder", text = "Deployment " .. event.status }, { type = "TextBlock", wrap = true, text = event.message }, }, }, }, }, } local body, err = json.encode(payload) if err then error(err) end r:SetRequestHeader("content-type", "application/json") r:SetRequestMethod("POST") r:SetRequestBody(body) ``` **Filter out noise.** Not every event deserves a ping. Use [forwarding rules](https://webhookrelay.com/blog/webhook-to-microsoft-teams/features/forwarding-rules/) to only deliver events that match a condition, or drop them in the function with an early `return`. **Fan out to multiple channels.** Add more destinations so one incoming webhook lands in several Teams channels — or in Teams _and_ Slack _and_ Discord — at the same time. See [forwarding to multiple destinations](https://webhookrelay.com/blog/webhook-to-microsoft-teams/features/webhook-multiple-destinations/). ## [FAQ](#faq) **What does Teams need to receive?** A `POST` with a JSON body. The minimum is `{ "text": "your message" }`; the modern Workflow path expects an Adaptive Card inside an `attachments` array. The transform function produces exactly the shape your URL expects. **Do I need to run a server?** No. Webhook Relay receives, transforms, and delivers everything in the cloud. There's nothing to host or keep online. **How do I see the raw payload my source sends?** Send it to a free [Webhook Bin](https://webhookrelay.com/blog/webhook-to-microsoft-teams/webhook-bin/) first to inspect the exact body and headers, then write your transform around that structure. ## [Get started](#get-started) [Create a free Webhook Relay account](https://my.webhookrelay.com/register) and set up your first "anything → Teams" bridge in a few minutes. Not sure what your source sends? [Inspect the payload in a Webhook Bin](https://webhookrelay.com/blog/webhook-to-microsoft-teams/webhook-bin/) first, then write the transform to match. --- --- title: Webhook to Telegram: Forward Any Payload | WebhookRelay meta: "og: title": "Webhook to Telegram: Forward Any Payload" description: Send a webhook to Telegram. Forward an incoming webhook into a chat, transforming the raw payload into the Bot API's sendMessage format in flight. url: https://webhookrelay.com/blog/webhook-to-telegram.md file: /blog/webhook-to-telegram.md --- ![Stripes](https://webhookrelay.com/blog/webhook-to-telegram/images/stripes.svg) # **Webhook to Telegram: Transform & Forward Any Payload to a Chat** Send a webhook to Telegram. Forward an incoming webhook into a chat, transforming the raw payload into the Bot API's sendMessage format in flight. ![Webhook to Telegram: Transform & Forward Any Payload to a Chat](https://webhookrelay.com/blog/webhook-to-telegram/images/blog/heroes/route.jpg) You have a service that fires webhooks — a CI pipeline, a contact form, an uptime monitor, a payment provider, an alerting tool — and you want each event to show up in a **Telegram chat**. The problem: Telegram doesn't accept arbitrary payloads. Its Bot API expects a specific JSON body, and the webhook your source sends almost never matches that shape. [Webhook Relay](https://webhookrelay.com) sits in the middle. It receives the incoming webhook at a stable public URL, **transforms** the payload into the format the Telegram Bot API expects, and delivers it — no glue server, no Lambda, no maintenance. ## [How it works](#how-it-works) The flow is a single hop with a transform in the middle: ``` Your service ──▶ Webhook Relay ──▶ transform to { chat_id, text } ──▶ Telegram chat (raw webhook) (public URL) (serverless function) (message appears) ``` To post a message, you call the Telegram Bot API's `sendMessage` method by POSTing JSON to: ``` https://api.telegram.org/bot/sendMessage ``` with a body like: ``` { "chat_id": "123456789", "text": "your message" } ``` A generic incoming webhook isn't shaped like that, so we add a small [transformation function](https://webhookrelay.com/blog/webhook-to-telegram/features/transform-webhooks/) that builds the `{ chat_id, text }` body from the raw payload before delivery. That one feature is what turns Webhook Relay into a universal "anything → Telegram" bridge. ## [Step 1: Create a bot and get its token](#step-1-create-a-bot-and-get-its-token) Open Telegram and start a chat with **@BotFather**. Send `/newbot`, give the bot a name and a username, and BotFather replies with an **HTTP API token** that looks like: ``` 123456789:AAExampleTokenStringFromBotFather ``` Keep this token secret — anyone who has it can send messages as your bot. ## [Step 2: Find your chat_id](#step-2-find-your-chat_id) Telegram won't let your bot message you until you've talked to it first, so send any message (like `/start`) to your new bot. Then fetch recent updates by opening this URL in a browser, replacing `` with yours: ``` https://api.telegram.org/bot/getUpdates ``` In the JSON response, find `result[].message.chat.id` — that number is your **chat_id**. For a group chat, add the bot to the group, post a message there, and read the (usually negative) group `chat.id` from the same `getUpdates` output. ## [Step 3: Create a Webhook Relay public endpoint](#step-3-create-a-webhook-relay-public-endpoint) Open [the new public destination page](https://my.webhookrelay.com/new-public-destination) and paste the Bot API URL as the **destination**: ``` https://api.telegram.org/bot/sendMessage ``` Webhook Relay gives you back an **input URL** — a public endpoint that you'll point your source service at. Everything sent to it gets forwarded toward Telegram. ## [Step 4: Add a transformation function](#step-4-add-a-transformation-function) Right now, anything you send to the input URL would be forwarded to `sendMessage` _as-is_ — and Telegram would ignore it, because it isn't in `{ "chat_id": "...", "text": "..." }` shape. Add a [transformation](https://webhookrelay.com/blog/webhook-to-telegram/features/transform-webhooks/) to fix that. Click **Transform** on the input, open the [Functions](https://my.webhookrelay.com/functions) page, create a function "from scratch", and paste this Lua: ``` local json = require("json") -- the chat the message should land in local chat_id = "123456789" local payload = { chat_id = chat_id, text = r.RequestBody, } local body, err = json.encode(payload) if err then error(err) end r:SetRequestHeader("content-type", "application/json") r:SetRequestMethod("POST") r:SetRequestBody(body) ``` This takes the raw incoming body as the message `text`, pairs it with your `chat_id`, sets the right headers and method, and lets Webhook Relay deliver it. Telegram now accepts the call and posts the message to your chat. ## [Step 5: Point your source at the URL and test](#step-5-point-your-source-at-the-url-and-test) Configure your source service (the CI tool, form, monitor, payment provider — whatever you're integrating) to send its webhook to the Webhook Relay **input URL**. To confirm everything works before wiring up the real service, fire a quick test with `curl`: ``` curl -X POST https://my.webhookrelay.com/v1/webhooks/your-input-id \ -d 'Deployment finished: build #482 passed on main' ``` Within a second or two, **Deployment finished: build #482 passed on main** appears in your Telegram chat. If nothing shows up, open the request log in your dashboard to see exactly what was received and delivered. ## [Going further](#going-further) Once the basic bridge works, the transform function is where the real power is. **Build a formatted message.** If your source sends JSON, parse it and construct a richer `text` string. Telegram supports Markdown and HTML formatting, so you can add bold headers and bullet points: ``` local json = require("json") local event = json.decode(r.RequestBody) local text = "*Deployment " .. event.status .. "*\n" .. event.message local payload = { chat_id = "123456789", text = text, parse_mode = "Markdown", } local body, err = json.encode(payload) if err then error(err) end r:SetRequestHeader("content-type", "application/json") r:SetRequestMethod("POST") r:SetRequestBody(body) ``` **Filter out noise.** Not every event deserves a ping. Use [forwarding rules](https://webhookrelay.com/blog/webhook-to-telegram/features/forwarding-rules/) to only deliver events that match a condition, or drop them in the function with an early `return`. **Fan out to multiple chats.** Add more destinations so one incoming webhook lands in several Telegram chats — or in Telegram _and_ Discord — at the same time. See [forwarding to multiple destinations](https://webhookrelay.com/blog/webhook-to-telegram/features/webhook-multiple-destinations/). The pattern here is identical to the one in [Send a webhook to Discord](https://webhookrelay.com/blog/webhook-to-telegram/blog/webhook-to-discord/) — only the destination URL and the body shape change. For a complete real-world walkthrough, see [TradingView alerts](https://webhookrelay.com/blog/webhook-to-telegram/blog/trading-view/), where a plain alert message is transformed into a chat notification. ## [FAQ](#faq) **What does Telegram need to receive?** A `POST` with a JSON body of `{ "chat_id": "...", "text": "your message" }` to `https://api.telegram.org/bot/sendMessage`. The transform function produces exactly that. **Do I need to run a server?** No. Webhook Relay receives, transforms, and delivers everything in the cloud. There's nothing to host or keep online. **How do I see the raw payload my source sends?** Send it to a free [Webhook Bin](https://webhookrelay.com/blog/webhook-to-telegram/webhook-bin/) first to inspect the exact body and headers, then write your transform around that structure. ## [Get started](#get-started) [Create a free Webhook Relay account](https://my.webhookrelay.com/register) and set up your first "anything → Telegram" bridge in a few minutes. Not sure what your source sends? [Inspect the payload in a Webhook Bin](https://webhookrelay.com/blog/webhook-to-telegram/webhook-bin/) first, then write the transform to match. --- --- title: Send a Webhook to Slack | WebhookRelay meta: "og: title": "Send a Webhook to Slack" description: Send any webhook to Slack. Forward an incoming webhook into a Slack channel, transforming the raw payload into Slack's text format in flight. url: https://webhookrelay.com/blog/webhook-to-slack.md file: /blog/webhook-to-slack.md --- ![Stripes](https://webhookrelay.com/blog/webhook-to-slack/images/stripes.svg) # **Send a Webhook to Slack** Send any webhook to Slack. Forward an incoming webhook into a Slack channel, transforming the raw payload into Slack's text format in flight. ![Send a Webhook to Slack: Transform & Forward Any Payload](https://webhookrelay.com/blog/webhook-to-slack/images/blog/heroes/route.jpg) You have a service that fires webhooks — a CI pipeline, a signup form, an uptime monitor, a payment provider, an alerting tool — and you want each event to show up in a **Slack channel**. The problem: Slack incoming webhooks don't accept arbitrary payloads. They expect a specific JSON body, and the webhook your source sends almost never matches that shape. [Webhook Relay](https://webhookrelay.com) sits in the middle. It receives the incoming webhook at a stable public URL, **transforms** the payload into the format Slack expects, and delivers it — no glue server, no Lambda, no maintenance. (Don't have one yet? See [how to get a Slack webhook URL](https://webhookrelay.com/blog/webhook-to-slack/blog/how-to-get-slack-webhook-url/).) > **Build it visually first:** the free [webhook → Slack/Discord/Teams formatter](https://webhookrelay.com/blog/webhook-to-slack/webhook-message-formatter/) lets you paste a sample payload, template the Slack message, preview it live, and copy the exact transform function used below — a faster, automatable [Discohook alternative](https://webhookrelay.com/blog/webhook-to-slack/webhook-message-formatter/) for incoming webhooks. ## [How it works](#how-it-works) The flow is a single hop with a transform in the middle: ``` Your service ──▶ Webhook Relay ──▶ transform to { text } ──▶ Slack channel (raw webhook) (public URL) (serverless function) (message appears) ``` A Slack incoming webhook expects a JSON body like this: ``` { "text": "your message" } ``` A generic incoming webhook isn't shaped like that, so we add a small [transformation function](https://webhookrelay.com/blog/webhook-to-slack/features/transform-webhooks/) that wraps the raw body into Slack's format before delivery. That one feature is what turns Webhook Relay into a universal "anything → Slack" bridge. ## [Step 1: Create the Slack incoming webhook URL](#step-1-create-the-slack-incoming-webhook-url) In Slack, you create an incoming webhook through a Slack app: 1. Go to [api.slack.com/apps](https://api.slack.com/apps) and click **Create New App → From scratch**. Name it and pick your workspace. 2. Open **Incoming Webhooks** and toggle it **On**. 3. Click **Add New Webhook to Workspace**, choose the channel messages should post to, and authorize it. 4. Copy the generated **Webhook URL**. It looks like: ``` https://hooks.slack.com/services/T0000/B0000/XXXXXXXX ``` Keep this URL handy — it's the destination Webhook Relay will deliver to. Each Slack incoming webhook is bound to one channel, so create one per channel you want to post to. ## [Step 2: Create a Webhook Relay public endpoint](#step-2-create-a-webhook-relay-public-endpoint) Open [the new public destination page](https://my.webhookrelay.com/new-public-destination) and paste the Slack incoming webhook URL as the **destination**. Webhook Relay gives you back an **input URL** — a public endpoint that you'll point your source service at. That input URL is now a stable address you can hand to any service. Everything sent to it gets forwarded toward Slack. ## [Step 3: Add a transformation function](#step-3-add-a-transformation-function) Right now, anything you send to the input URL would be forwarded to Slack _as-is_ — and Slack would reject it, because it isn't in `{ "text": "..." }` shape. Add a [transformation](https://webhookrelay.com/blog/webhook-to-slack/features/transform-webhooks/) to fix that. Click **Transform** on the input, open the [Functions](https://my.webhookrelay.com/functions) page, create a function "from scratch", and paste this Lua: ``` local json = require("json") local payload = { text = r.RequestBody, } local body, err = json.encode(payload) if err then error(err) end r:SetRequestHeader("content-type", "application/json") r:SetRequestMethod("POST") r:SetRequestBody(body) ``` This takes the raw incoming body, wraps it as `{ "text": "..." }`, sets the right headers and method, and lets Webhook Relay deliver it. Slack now accepts the message and posts it to your channel. ## [Step 4: Point your source at the URL and test](#step-4-point-your-source-at-the-url-and-test) Configure your source service (the CI tool, form, monitor, payment provider — whatever you're integrating) to send its webhook to the Webhook Relay **input URL**. To confirm everything works before wiring up the real service, fire a quick test with `curl`: ``` curl -X POST https://my.webhookrelay.com/v1/webhooks/your-input-id \ -d 'Deployment finished: build #482 passed on main' ``` Within a second or two, **Deployment finished: build #482 passed on main** appears in your Slack channel. If nothing shows up, open the request log in your dashboard to see exactly what was received and delivered. ## [Going further](#going-further) Once the basic bridge works, the transform function is where the real power is. **Build a richer message with Block Kit.** If your source sends JSON, parse it and construct a Slack `blocks` array instead of a flat string — sections, labelled fields, dividers, even buttons: ``` local json = require("json") local event = json.decode(r.RequestBody) local payload = { blocks = { { type = "section", text = { type = "mrkdwn", text = "*Deployment " .. event.status .. "*\n" .. event.message, }, }, }, } local body, err = json.encode(payload) if err then error(err) end r:SetRequestHeader("content-type", "application/json") r:SetRequestMethod("POST") r:SetRequestBody(body) ``` **Filter out noise.** Not every event deserves a ping. Use [forwarding rules](https://webhookrelay.com/blog/webhook-to-slack/features/forwarding-rules/) to only deliver events that match a condition, or drop them in the function with an early `return`. **Fan out to multiple channels.** Add more destinations so one incoming webhook lands in several Slack channels — or in Slack _and_ Discord — at the same time. Since each Slack incoming webhook targets one channel, add one destination per channel. See [forwarding to multiple destinations](https://webhookrelay.com/blog/webhook-to-slack/features/webhook-multiple-destinations/). For a complete real-world walkthrough of this transform pattern, see [TradingView alerts to Discord and Slack](https://webhookrelay.com/blog/webhook-to-slack/blog/trading-view/), where a plain alert message is reshaped into a chat notification. ## [FAQ](#faq) **What does Slack need to receive?** A `POST` with a JSON body of `{ "text": "your message" }` (or a `blocks` array for Block Kit) to a Slack incoming webhook URL. The transform function produces exactly that. **Do I need to run a server?** No. Webhook Relay receives, transforms, and delivers everything in the cloud. There's nothing to host or keep online. **How do I see the raw payload my source sends?** Send it to a free [Webhook Bin](https://webhookrelay.com/blog/webhook-to-slack/webhook-bin/) first to inspect the exact body and headers, then write your transform around that structure. ## [Get started](#get-started) [Create a free Webhook Relay account](https://my.webhookrelay.com/register) and set up your first "anything → Slack" bridge in a few minutes. Not sure what your source sends? [Inspect the payload in a Webhook Bin](https://webhookrelay.com/blog/webhook-to-slack/webhook-bin/) first, then write the transform to match. --- --- title: Send a Webhook as Email: Forward Any Event to Your Inbox meta: "og: title": "Send a Webhook as Email: Forward Any Event to Your Inbox" description: Turn incoming webhooks into email notifications. Forward an event, transform it in flight, and send a formatted email via SendGrid, Mailgun or Postmark. url: https://webhookrelay.com/blog/webhook-to-email.md file: /blog/webhook-to-email.md --- ![Stripes](https://webhookrelay.com/blog/webhook-to-email/images/stripes.svg) # **Send a Webhook as Email: Forward Any Event to Your Inbox** Turn incoming webhooks into email notifications. Forward an event, transform it in flight, and send a formatted email via SendGrid, Mailgun or Postmark. ![Send a Webhook as Email: Forward Any Event to Your Inbox](https://webhookrelay.com/blog/webhook-to-email/images/blog/heroes/route.jpg) You have a service that fires webhooks — a payment provider, a CI pipeline, a form, a monitor, an alerting tool — and you'd rather get a plain **email** than wire up yet another dashboard or chat integration. Email is universal: everyone has an inbox, it's searchable, and it's easy to forward to a teammate. The catch is that webhooks aren't emails. A provider POSTs a JSON payload to a URL; turning that into a nicely formatted message in your inbox normally means standing up a small server with SMTP or an email API. [Webhook Relay](https://webhookrelay.com) removes that step: it receives the webhook at a stable public URL, runs a **transform function** that formats _and sends_ the email, and you host nothing. ## [How it works](#how-it-works) The flow is a single hop with a transform that sends the email: ``` Your service ──▶ Webhook Relay ──▶ transform: format + send email ──▶ your inbox (raw webhook) (public URL) (serverless function) (email arrives) ``` The [transformation function](https://webhookrelay.com/blog/webhook-to-email/features/transform-webhooks/) does two jobs: it shapes the payload into a human-readable message, and it dispatches that message as an email — either through a built-in send-email capability or by calling an email API such as **Mailgun**, **SendGrid** or **Postmark**. ## [Step 1: Create a Webhook Relay endpoint](#step-1-create-a-webhook-relay-endpoint) Create a bucket and input at [my.webhookrelay.com/buckets](https://my.webhookrelay.com/buckets). Copy the **input URL** (it looks like `https://xyz.hooks.webhookrelay.com`) — that's the public address you'll point your source service at. You don't need a forwarding destination here: the function itself sends the email, so the input alone is enough. ## [Step 2: Inspect what your source sends](#step-2-inspect-what-your-source-sends) Before writing the function, capture a real payload so you know the field names. Point your source at a free [Webhook Bin](https://webhookrelay.com/blog/webhook-to-email/webhook-bin/) (or temporarily at the input URL and read the request log) and trigger one event. Now you can see the exact JSON and write your formatting around the real structure instead of guessing. ## [Step 3: Add a transform that formats and sends the email](#step-3-add-a-transform-that-formats-and-sends-the-email) Open the [Functions](https://my.webhookrelay.com/functions) page, create a function "from scratch", and attach it to your input. The example below uses **Mailgun**. First, on the function's **config variables** tab, set: - **api_key** — your Mailgun API key - **domain** — your Mailgun sending domain (e.g. `mg.example.com`) Then paste the function body, replacing the sender and recipient addresses: ``` local mailgun = require("mailgun") local json = require("json") -- Decode the incoming webhook so we can pull out fields local event, err = json.decode(r.RequestBody) if err then error(err) end -- Build a readable message from whatever your source sent local subject = "Webhook: " .. (event.type or "new event") local text = string.format([[ A new event arrived. Type: %s Message: %s Raw payload: %s ]], event.type or "unknown", event.message or "(none)", r.RequestBody) -- Initialise Mailgun with your config variables err = mailgun.initialize(cfg:GetValue("domain"), cfg:GetValue("api_key"), "us") if err then error(err) end -- Send the email err = mailgun.send( "from-address@example.com", subject, text, "to-address@example.com" ) if err then error(err) end r:SetRequestBody("email sent") ``` That's the whole integration. Every webhook that hits the input is decoded, formatted, and emailed — no server, no SMTP setup, no cron. > Prefer a different provider? The same pattern works by POSTing to an email API from the function. Build the request body for [SendGrid](https://sendgrid.com), [Postmark](https://postmarkapp.com) or another service, set the `Authorization` header from a config variable, and the function delivers it. The structure (decode → format → send) is identical. ## [Step 4: Filter so you only get useful email](#step-4-filter-so-you-only-get-useful-email) The fastest way to ruin email notifications is to send too many. Filter inside the function. For example, skip low-value events and drop the request entirely: ``` -- Only email on the events you care about if event.type ~= "payment.succeeded" then r:StopForwarding() return end ``` `StopForwarding` stops the request right there, so no email goes out and nothing is forwarded. You can also push this upstream with [forwarding rules](https://webhookrelay.com/blog/webhook-to-email/features/forwarding-rules/) so only matching events ever reach the function. ## [A concrete example: Stripe → email](#a-concrete-example-stripe-email) A classic use of this is emailing yourself when a new customer subscribes. The decode-format-filter-send pattern above is exactly what powers the [Stripe webhook to email walkthrough](https://webhookrelay.com/blog/webhook-to-email/blog/stripe-webhook-to-email/), which filters out free-plan signups and emails you the plan, amount, and links straight into the Stripe dashboard on every new paying customer. Swap Stripe for any provider and the shape of the function stays the same. ## [Going further](#going-further) - **Richer formatting.** Send HTML email by passing an HTML body to your provider, so you can include headings, tables and links instead of plain text. - **Route by content.** Inspect the payload and choose the recipient dynamically — `payment` events to finance, `error` events to on-call. - **Email _and_ more.** A webhook doesn't have to go just one place. Fan the same event out to email plus a chat channel or another API at once with [multiple destinations](https://webhookrelay.com/blog/webhook-to-email/features/webhook-multiple-destinations/). ## [FAQ](#faq) **What do I need besides Webhook Relay?** An email provider account (Mailgun, SendGrid, Postmark, etc.) and its API key. You store the key as a config variable on the function — it never goes near your source service. **Will this work with any webhook provider?** Yes. Anything that can POST to a URL works — payment providers, CI systems, monitors, forms, IoT devices. The function decodes whatever JSON arrives; inspect a real payload in a [Webhook Bin](https://webhookrelay.com/blog/webhook-to-email/webhook-bin/) first to learn the field names. **Do I need to host anything?** No. The function — including the email send — runs on Webhook Relay in the cloud. There's nothing to deploy or keep online. ## [Get started](#get-started) [Create a free Webhook Relay account](https://my.webhookrelay.com/register), attach a [transform function](https://webhookrelay.com/blog/webhook-to-email/features/transform-webhooks/), and turn your next webhook into an email. Not sure what your source sends? [Inspect the payload in a Webhook Bin](https://webhookrelay.com/blog/webhook-to-email/webhook-bin/) first, then write the function to match. --- --- title: Webhook to Google Sheets: Forward Any Payload | WebhookRelay meta: "og: title": "Webhook to Google Sheets: Forward Any Payload" description: Send a webhook to Google Sheets. Forward an incoming webhook into a spreadsheet with an Apps Script Web App, transforming the payload in flight. url: https://webhookrelay.com/blog/webhook-to-google-sheets.md file: /blog/webhook-to-google-sheets.md --- ![Stripes](https://webhookrelay.com/blog/webhook-to-google-sheets/images/stripes.svg) # **Webhook to Google Sheets: Transform & Forward Any Payload to a Row** Send a webhook to Google Sheets. Forward an incoming webhook into a spreadsheet with an Apps Script Web App, transforming the payload in flight. ![Webhook to Google Sheets: Transform & Forward Any Payload to a Row](https://webhookrelay.com/blog/webhook-to-google-sheets/images/blog/heroes/route.jpg) You have a service that fires webhooks — a contact form, a payment provider, a sign-up flow, a CI pipeline, a monitoring tool — and you want each event logged as a row in **Google Sheets**. The problem: Sheets has no native webhook endpoint, and even when you build one, the webhook your source sends almost never matches the columns you want. [Webhook Relay](https://webhookrelay.com) sits in the middle. It receives the incoming webhook at a stable public URL, **transforms** the payload into the shape your spreadsheet bridge expects, and delivers it — no glue server, no Lambda, no maintenance. ## [How it works](#how-it-works) The flow is a single hop with a transform in the middle: ``` Your service ──▶ Webhook Relay ──▶ transform to { ...fields } ──▶ Apps Script Web App ──▶ new row (raw webhook) (public URL) (serverless function) (doPost endpoint) (in your sheet) ``` Google Sheets can't receive a webhook on its own. The clean, no-OAuth way to get data in is a **Google Apps Script Web App**: a `doPost(e)` function that runs in the context of your sheet, appends a row, and is published at a public URL. That URL becomes the Webhook Relay destination, and a small [transformation function](https://webhookrelay.com/blog/webhook-to-google-sheets/features/transform-webhooks/) reshapes each incoming payload into the fields the script writes. ## [Step 1: Create the sheet and the Apps Script](#step-1-create-the-sheet-and-the-apps-script) Create a new Google Sheet. Then open **Extensions → Apps Script** and replace the default code with a `doPost` handler that parses the incoming JSON and appends a row: ``` function doPost(e) { const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0]; const data = JSON.parse(e.postData.contents); sheet.appendRow([ new Date(), data.event || "", data.message || "", ]); return ContentService .createTextOutput(JSON.stringify({ status: "ok" })) .setMimeType(ContentService.MimeType.JSON); } ``` This appends a row with a timestamp plus whatever `event` and `message` fields arrive in the JSON body. Adjust the columns to whatever you want to capture. ## [Step 2: Deploy it as a Web App](#step-2-deploy-it-as-a-web-app) In the Apps Script editor, click **Deploy → New deployment**, choose type **Web app**, and set: - **Execute as:** Me - **Who has access:** Anyone Click **Deploy**, authorize the script when prompted, and copy the **Web app URL**. It looks like: ``` https://script.google.com/macros/s/AKfyc.../exec ``` That public URL is the destination Webhook Relay will deliver to. Note: every time you change the script, you must publish a **new version** of the deployment for the change to take effect. ## [Step 3: Create a Webhook Relay public endpoint](#step-3-create-a-webhook-relay-public-endpoint) Open [the new public destination page](https://my.webhookrelay.com/new-public-destination) and paste the Apps Script **Web app URL** as the **destination**. Webhook Relay gives you back an **input URL** — a public endpoint that you'll point your source service at. Everything sent to it gets forwarded toward your sheet. ## [Step 4: Add a transformation function](#step-4-add-a-transformation-function) If your source already sends JSON with exactly the fields your `doPost` reads, you can forward it untouched. More often the incoming payload has different field names, so add a [transformation](https://webhookrelay.com/blog/webhook-to-google-sheets/features/transform-webhooks/) to reshape it. Click **Transform** on the input, open the [Functions](https://my.webhookrelay.com/functions) page, create a function "from scratch", and paste this Lua: ``` local json = require("json") -- incoming payload from your source service local incoming = json.decode(r.RequestBody) -- reshape into the fields the Apps Script expects local payload = { event = incoming.type, message = incoming.summary, } local body, err = json.encode(payload) if err then error(err) end r:SetRequestHeader("content-type", "application/json") r:SetRequestMethod("POST") r:SetRequestBody(body) ``` This decodes the raw incoming body, maps its fields onto the `event` and `message` keys the script writes, sets the right headers and method, and lets Webhook Relay deliver it. The Web App appends the row. If you don't need to reshape anything, you can pass the body straight through instead: ``` r:SetRequestHeader("content-type", "application/json") r:SetRequestMethod("POST") r:SetRequestBody(r.RequestBody) ``` ## [Step 5: Point your source at the URL and test](#step-5-point-your-source-at-the-url-and-test) Configure your source service (the form, payment provider, monitor — whatever you're integrating) to send its webhook to the Webhook Relay **input URL**. To confirm everything works before wiring up the real service, fire a quick test with `curl`: ``` curl -X POST https://my.webhookrelay.com/v1/webhooks/your-input-id \ -H 'content-type: application/json' \ -d '{"type":"signup","summary":"New trial: acme.com"}' ``` Within a second or two, a new row — timestamp, `signup`, `New trial: acme.com` — appears in your sheet. If nothing shows up, open the request log in your dashboard to see exactly what was received and delivered. ## [Going further](#going-further) Once the basic bridge works, the transform function is where the real power is. **Capture more columns.** Add fields in both the Apps Script `appendRow([...])` call and the Lua `payload` table — amounts, emails, IDs, statuses — to build a full audit log from your webhooks. **Enrich the row before writing.** A transform function can call any HTTP API, so you can look up extra data (a customer name, a geolocation) and add it as a column before the row is appended. **Filter out noise.** Not every event deserves a row. Use [forwarding rules](https://webhookrelay.com/blog/webhook-to-google-sheets/features/forwarding-rules/) to only deliver events that match a condition, or drop them in the function with an early `return`. **Fan out.** Add more destinations so one incoming webhook lands in a sheet _and_ a chat at the same time. See [forwarding to multiple destinations](https://webhookrelay.com/blog/webhook-to-google-sheets/features/webhook-multiple-destinations/). The pattern here is identical to the one in [Send a webhook to Discord](https://webhookrelay.com/blog/webhook-to-google-sheets/blog/webhook-to-discord/) — only the destination URL and the body shape change. For another real-world walkthrough, see [TradingView alerts](https://webhookrelay.com/blog/webhook-to-google-sheets/blog/trading-view/). ## [FAQ](#faq) **Why an Apps Script Web App instead of the Sheets API?** Because it needs no OAuth setup on your side. The Web App runs as you, has direct access to the spreadsheet, and exposes a single public URL that Webhook Relay can POST to. **Do I need to run a server?** No. Webhook Relay receives, transforms, and delivers everything in the cloud, and the Apps Script runs on Google's infrastructure. There's nothing to host or keep online. **How do I see the raw payload my source sends?** Send it to a free [Webhook Bin](https://webhookrelay.com/blog/webhook-to-google-sheets/webhook-bin/) first to inspect the exact body and headers, then write your `doPost` columns and transform around that structure. ## [Get started](#get-started) [Create a free Webhook Relay account](https://my.webhookrelay.com/register) and set up your first "anything → Google Sheets" bridge in a few minutes. Not sure what your source sends? [Inspect the payload in a Webhook Bin](https://webhookrelay.com/blog/webhook-to-google-sheets/webhook-bin/) first, then write the transform to match. --- --- title: Send a Webhook to PagerDuty: Trigger Incidents meta: "og: title": "Send a Webhook to PagerDuty: Trigger Incidents" description: Send any webhook to PagerDuty. Transform an incoming webhook into a PagerDuty Events API v2 alert in flight to trigger or resolve incidents. url: https://webhookrelay.com/blog/webhook-to-pagerduty.md file: /blog/webhook-to-pagerduty.md --- ![Stripes](https://webhookrelay.com/blog/webhook-to-pagerduty/images/stripes.svg) # **Send a Webhook to PagerDuty: Trigger Incidents From Any Event** Send any webhook to PagerDuty. Transform an incoming webhook into a PagerDuty Events API v2 alert in flight to trigger or resolve incidents. ![Send a Webhook to PagerDuty: Trigger Incidents From Any Event](https://webhookrelay.com/blog/webhook-to-pagerduty/images/blog/heroes/route.jpg) You have a tool that fires webhooks when something goes wrong — an uptime monitor, a CI pipeline, a custom health check, a security scanner — and you want each event to open (or resolve) an incident in **PagerDuty**. The problem: the PagerDuty Events API expects a precise JSON body with a `routing_key` and an `event_action`, and the webhook your tool sends never matches that shape. [Webhook Relay](https://webhookrelay.com) sits in the middle. It receives the incoming webhook at a stable public URL, **transforms** the payload into PagerDuty's Events API v2 format, and delivers it — no glue server, no Lambda, no maintenance. ## [How it works](#how-it-works) 1. Your tool POSTs its webhook to a Webhook Relay endpoint. 2. A transformation function maps the payload into a PagerDuty Events API v2 request. 3. Webhook Relay forwards it to `https://events.pagerduty.com/v2/enqueue`, and an incident triggers, acknowledges or resolves. ## [Step 1: Get a PagerDuty routing key](#step-1-get-a-pagerduty-routing-key) In PagerDuty, open (or create) a **service**, add an **Events API v2** integration, and copy its **Integration Key** (the routing key). ## [Step 2: Create a Webhook Relay output to the Events API](#step-2-create-a-webhook-relay-output-to-the-events-api) Create a bucket with a public input, then add an output pointing at the PagerDuty Events API: - **Output destination:** `https://events.pagerduty.com/v2/enqueue` - **Headers:** `Content-Type: application/json` ## [Step 3: Add a transformation function](#step-3-add-a-transformation-function) Attach a function that maps the incoming webhook into the Events API v2 format: ``` local body = json.decode(r.RequestBody) local event = { routing_key = "YOUR_ROUTING_KEY", event_action = "trigger", dedup_key = body.id, -- stable key so repeats update one incident payload = { summary = body.message or "Alert from webhook", source = body.source or "webhook-relay", severity = body.severity or "critical" } } r:SetRequestBody(json.encode(event)) ``` Send `event_action = "resolve"` with the same `dedup_key` to close the incident automatically when the source recovers. ## [Step 4: Point your source at the URL and test](#step-4-point-your-source-at-the-url-and-test) Configure your monitoring or CI tool's webhook to point at the Webhook Relay public URL. Trigger an event — or replay one from the [Webhook Bin](https://webhookrelay.com/blog/webhook-to-pagerduty/webhook-bin/) — and an incident appears in PagerDuty within seconds. The Webhook Relay logs show PagerDuty's exact response if anything is off. ## [Going further](#going-further) - **Fan out:** also post the alert to [Slack](https://webhookrelay.com/blog/webhook-to-pagerduty/blog/webhook-to-slack/), [Microsoft Teams](https://webhookrelay.com/blog/webhook-to-pagerduty/blog/webhook-to-microsoft-teams/) or [Mattermost](https://webhookrelay.com/blog/webhook-to-pagerduty/blog/webhook-to-mattermost/) with [multiple destinations](https://webhookrelay.com/blog/webhook-to-pagerduty/features/webhook-multiple-destinations/). - **Filter noise:** use [forwarding rules](https://webhookrelay.com/blog/webhook-to-pagerduty/features/forwarding-rules/) so only real alerts page someone. - **Inspect first:** see the raw payload in the [Webhook Bin](https://webhookrelay.com/blog/webhook-to-pagerduty/webhook-bin/) before writing the mapping. ## [Get started](#get-started) [Create a free Webhook Relay account](https://my.webhookrelay.com/register) and turn any webhook into PagerDuty incidents — no servers to run. New to webhooks? Start with [what is a webhook](https://webhookrelay.com/blog/webhook-to-pagerduty/blog/what-is-webhook/) and [how to transform webhooks](https://webhookrelay.com/blog/webhook-to-pagerduty/features/transform-webhooks/). --- --- title: Send a Webhook to Opsgenie | WebhookRelay meta: "og: title": "Send a Webhook to Opsgenie" description: Send any webhook to Opsgenie. Transform an incoming webhook into the Opsgenie API format in flight and forward it — no glue server, no code to maintain. url: https://webhookrelay.com/blog/webhook-to-opsgenie.md file: /blog/webhook-to-opsgenie.md --- ![Stripes](https://webhookrelay.com/blog/webhook-to-opsgenie/images/stripes.svg) # **Send a Webhook to Opsgenie** Send any webhook to Opsgenie. Transform an incoming webhook into the Opsgenie API format in flight and forward it — no glue server, no code to maintain. ![Send a Webhook to Opsgenie: Transform & Forward Any Payload](https://webhookrelay.com/blog/webhook-to-opsgenie/images/blog/heroes/route.jpg) You have a service that fires webhooks — a form, a payment provider, a CI pipeline, a monitoring tool — and you want each event to open an alert in **Opsgenie**. The problem: the Opsgenie API won't accept the raw webhook. It expects its own JSON shape and authentication, and the payload your source sends never matches. [Webhook Relay](https://webhookrelay.com) sits in the middle. It receives the incoming webhook at a stable public URL, **transforms** the payload into the format Opsgenie expects, and delivers it — no glue server, no Lambda, no maintenance. ## [How it works](#how-it-works) 1. Your source service POSTs its webhook to a Webhook Relay endpoint. 2. A transformation function parses the payload and builds a Opsgenie API request. 3. Webhook Relay forwards it to Opsgenie, and the record is created. ## [Step 1: Get your Opsgenie credentials](#step-1-get-your-opsgenie-credentials) You need an **API key** from an Opsgenie API integration (use `api.eu.opsgenie.com` on the EU instance). ## [Step 2: Create a Webhook Relay output to the Opsgenie API](#step-2-create-a-webhook-relay-output-to-the-opsgenie-api) Create a bucket with a public input, then add an output pointing at the Opsgenie API: - **Output destination:** `https://api.opsgenie.com/v2/alerts` - **Headers:** - `Authorization: GenieKey ` - `Content-Type: application/json` ## [Step 3: Add a transformation function](#step-3-add-a-transformation-function) Attach a function that reshapes the incoming webhook. Opsgenie's Alert API expects a `message` (required), plus optional `description`, `priority` (`P1`–`P5`) and an `alias` for deduplication. ``` -- incoming payload is in r.RequestBody local body = json.decode(r.RequestBody) local alert = { message = body.title or "Alert from webhook", description = body.message or r.RequestBody, alias = body.id, -- stable alias so repeats update one alert priority = body.priority or "P3" } r:SetRequestBody(json.encode(alert)) ``` ## [Step 4: Point your source at the URL and test](#step-4-point-your-source-at-the-url-and-test) Configure your source service's webhook to point at the Webhook Relay public URL. Trigger an event — or replay one from the [Webhook Bin](https://webhookrelay.com/blog/webhook-to-opsgenie/webhook-bin/) — and the record appears in Opsgenie within seconds. If the API rejects the request, the Webhook Relay logs show Opsgenie's exact error so you can fix the mapping. ## [Going further](#going-further) - **Fan out:** send the same event to [PagerDuty](https://webhookrelay.com/blog/webhook-to-opsgenie/blog/webhook-to-pagerduty/) or [Slack](https://webhookrelay.com/blog/webhook-to-opsgenie/blog/webhook-to-slack/) at the same time with [multiple destinations](https://webhookrelay.com/blog/webhook-to-opsgenie/features/webhook-multiple-destinations/). - **Filter:** use [forwarding rules](https://webhookrelay.com/blog/webhook-to-opsgenie/features/forwarding-rules/) so only the events you care about reach Opsgenie. - **Inspect first:** see the raw payload in the [Webhook Bin](https://webhookrelay.com/blog/webhook-to-opsgenie/webhook-bin/) before writing the mapping. ## [Get started](#get-started) [Create a free Webhook Relay account](https://my.webhookrelay.com/register) and turn any webhook into Opsgenie records — no servers to run. New to webhooks? Start with [what is a webhook](https://webhookrelay.com/blog/webhook-to-opsgenie/blog/what-is-webhook/) and [how to transform webhooks](https://webhookrelay.com/blog/webhook-to-opsgenie/features/transform-webhooks/). --- --- title: Send a Webhook to Datadog: Post Events From Any Source meta: "og: title": "Send a Webhook to Datadog: Post Events From Any Source" description: Send any webhook to Datadog. Transform an incoming webhook into a Datadog Events API request in flight to post events and alerts — no glue server. url: https://webhookrelay.com/blog/webhook-to-datadog.md file: /blog/webhook-to-datadog.md --- ![Stripes](https://webhookrelay.com/blog/webhook-to-datadog/images/stripes.svg) # **Send a Webhook to Datadog: Post Events From Any Source** Send any webhook to Datadog. Transform an incoming webhook into a Datadog Events API request in flight to post events and alerts — no glue server. ![Send a Webhook to Datadog: Post Events From Any Source](https://webhookrelay.com/blog/webhook-to-datadog/images/blog/heroes/route.jpg) You want events from across your stack — deploys, signups, failed payments, CI runs — to show up on your **Datadog** Events stream so you can overlay them on dashboards and alert on them. The problem: the Datadog Events API expects a specific JSON body and the API key in a header, and the webhook your source sends never matches that shape. [Webhook Relay](https://webhookrelay.com) sits in the middle. It receives the incoming webhook at a stable public URL, **transforms** the payload into the Datadog Events API format, and delivers it — no glue server, no Lambda, no maintenance. ## [How it works](#how-it-works) 1. Your source service POSTs its webhook to a Webhook Relay endpoint. 2. A transformation function maps the payload into a Datadog Events API request. 3. Webhook Relay forwards it to the Datadog Events API with your API key, and the event appears in Datadog. ## [Step 1: Get a Datadog API key](#step-1-get-a-datadog-api-key) In Datadog, go to **Organization Settings → API Keys** and copy (or create) an **API key**. ## [Step 2: Create a Webhook Relay output to the Events API](#step-2-create-a-webhook-relay-output-to-the-events-api) Create a bucket with a public input, then add an output pointing at the Datadog Events API: - **Output destination:** `https://api.datadoghq.com/api/v1/events` (use `datadoghq.eu` for the EU site) - **Headers:** - `DD-API-KEY: ` - `Content-Type: application/json` ## [Step 3: Add a transformation function](#step-3-add-a-transformation-function) Attach a function that maps the incoming webhook into a Datadog event: ``` local body = json.decode(r.RequestBody) local event = { title = body.title or "Event from webhook", text = body.message or r.RequestBody, alert_type = body.severity or "info", -- info | warning | error | success tags = { "source:webhook-relay", "service:" .. (body.service or "unknown") } } r:SetRequestBody(json.encode(event)) ``` ## [Step 4: Point your source at the URL and test](#step-4-point-your-source-at-the-url-and-test) Configure your source service's webhook to point at the Webhook Relay public URL. Trigger an event — or replay one from the [Webhook Bin](https://webhookrelay.com/blog/webhook-to-datadog/webhook-bin/) — and it appears on your Datadog Events stream within seconds. The Webhook Relay logs show Datadog's response if the mapping needs tweaking. ## [Going further](#going-further) - **Fan out:** also send the event to [PagerDuty](https://webhookrelay.com/blog/webhook-to-datadog/blog/webhook-to-pagerduty/), [Slack](https://webhookrelay.com/blog/webhook-to-datadog/blog/webhook-to-slack/) or archive it to [S3](https://webhookrelay.com/blog/webhook-to-datadog/blog/webhook-to-s3/) with [multiple destinations](https://webhookrelay.com/blog/webhook-to-datadog/features/webhook-multiple-destinations/). - **Filter noise:** use [forwarding rules](https://webhookrelay.com/blog/webhook-to-datadog/features/forwarding-rules/) so only the events you care about reach Datadog. - **Inspect first:** see the raw payload in the [Webhook Bin](https://webhookrelay.com/blog/webhook-to-datadog/webhook-bin/) before writing the mapping. ## [Get started](#get-started) [Create a free Webhook Relay account](https://my.webhookrelay.com/register) and turn any webhook into Datadog events — no servers to run. New to webhooks? Start with [what is a webhook](https://webhookrelay.com/blog/webhook-to-datadog/blog/what-is-webhook/) and [how to transform webhooks](https://webhookrelay.com/blog/webhook-to-datadog/features/transform-webhooks/). --- --- title: Send a Webhook to Mattermost | WebhookRelay meta: "og: title": "Send a Webhook to Mattermost" description: Send any webhook to Mattermost. Forward an incoming webhook into a channel, transforming the raw payload into the Mattermost webhook format in flight. url: https://webhookrelay.com/blog/webhook-to-mattermost.md file: /blog/webhook-to-mattermost.md --- ![Stripes](https://webhookrelay.com/blog/webhook-to-mattermost/images/stripes.svg) # **Send a Webhook to Mattermost** Send any webhook to Mattermost. Forward an incoming webhook into a channel, transforming the raw payload into the Mattermost webhook format in flight. ![Send a Webhook to Mattermost: Transform & Forward Any Payload](https://webhookrelay.com/blog/webhook-to-mattermost/images/blog/heroes/route.jpg) You have a service that fires webhooks — a CI pipeline, an uptime monitor, a payment provider, an alerting tool — and you want each event to show up in a **Mattermost channel**. The problem: Mattermost incoming webhooks expect a specific JSON body, and the webhook your source sends almost never matches that shape. [Webhook Relay](https://webhookrelay.com) sits in the middle. It receives the incoming webhook at a stable public URL, **transforms** the payload into the format Mattermost expects, and delivers it — including to **self-hosted** Mattermost servers on a private network, since the agent connects outbound. ## [How it works](#how-it-works) 1. Your source service POSTs its webhook to a Webhook Relay endpoint. 2. A transformation function wraps the payload into a Mattermost message. 3. Webhook Relay forwards it to your Mattermost incoming webhook URL, and the message posts to the channel. ## [Step 1: Create the Mattermost incoming webhook URL](#step-1-create-the-mattermost-incoming-webhook-url) In Mattermost, go to **Integrations → Incoming Webhooks → Add Incoming Webhook**, pick a channel, and copy the generated URL (it looks like `https://your-mattermost/hooks/xxxxxxxx`). ## [Step 2: Create a Webhook Relay output to Mattermost](#step-2-create-a-webhook-relay-output-to-mattermost) Create a bucket with a public input, then add an output pointing at your Mattermost webhook URL: - **Output destination:** `https://your-mattermost/hooks/xxxxxxxx` - **Headers:** `Content-Type: application/json` ## [Step 3: Add a transformation function](#step-3-add-a-transformation-function) Attach a function that wraps the incoming payload into Mattermost's `text` format: ``` local body = json.decode(r.RequestBody) local message = { username = "Webhook Relay", text = "**" .. (body.title or "New event") .. "**\n" .. (body.message or r.RequestBody) } r:SetRequestBody(json.encode(message)) ``` For richer messages, build a Mattermost `attachments` array (Slack-compatible) with colors, fields and titles instead of plain text. ## [Step 4: Point your source at the URL and test](#step-4-point-your-source-at-the-url-and-test) Configure your source service's webhook to point at the Webhook Relay public URL. Trigger an event — or replay one from the [Webhook Bin](https://webhookrelay.com/blog/webhook-to-mattermost/webhook-bin/) — and the message posts to Mattermost within seconds. ## [Going further](#going-further) - **Fan out:** post the same event to [Slack](https://webhookrelay.com/blog/webhook-to-mattermost/blog/webhook-to-slack/), [Microsoft Teams](https://webhookrelay.com/blog/webhook-to-mattermost/blog/webhook-to-microsoft-teams/) or page someone via [PagerDuty](https://webhookrelay.com/blog/webhook-to-mattermost/blog/webhook-to-pagerduty/) with [multiple destinations](https://webhookrelay.com/blog/webhook-to-mattermost/features/webhook-multiple-destinations/). - **Self-host friendly:** deliver to private Mattermost servers with no public IP — see [forwarding webhooks to internal servers](https://webhookrelay.com/blog/webhook-to-mattermost/features/webhook-to-internal-server/). - **Inspect first:** see the raw payload in the [Webhook Bin](https://webhookrelay.com/blog/webhook-to-mattermost/webhook-bin/) before writing the mapping. ## [Get started](#get-started) [Create a free Webhook Relay account](https://my.webhookrelay.com/register) and turn any webhook into Mattermost messages — no servers to run. New to webhooks? Start with [what is a webhook](https://webhookrelay.com/blog/webhook-to-mattermost/blog/what-is-webhook/) and [how to transform webhooks](https://webhookrelay.com/blog/webhook-to-mattermost/features/transform-webhooks/). --- --- title: Send a Webhook to HubSpot | WebhookRelay meta: "og: title": "Send a Webhook to HubSpot" description: Send any webhook to HubSpot. Transform an incoming webhook into the HubSpot API format in flight and forward it — no glue server, no code to maintain. url: https://webhookrelay.com/blog/webhook-to-hubspot.md file: /blog/webhook-to-hubspot.md --- ![Stripes](https://webhookrelay.com/blog/webhook-to-hubspot/images/stripes.svg) # **Send a Webhook to HubSpot** Send any webhook to HubSpot. Transform an incoming webhook into the HubSpot API format in flight and forward it — no glue server, no code to maintain. ![Send a Webhook to HubSpot: Transform & Forward Any Payload](https://webhookrelay.com/blog/webhook-to-hubspot/images/blog/heroes/route.jpg) You have a service that fires webhooks — a form, a payment provider, a CI pipeline, a monitoring tool — and you want each event to create or update a contact in **HubSpot**. The problem: the HubSpot API won't accept the raw webhook. It expects its own JSON shape and authentication, and the payload your source sends never matches. [Webhook Relay](https://webhookrelay.com) sits in the middle. It receives the incoming webhook at a stable public URL, **transforms** the payload into the format HubSpot expects, and delivers it — no glue server, no Lambda, no maintenance. ## [How it works](#how-it-works) 1. Your source service POSTs its webhook to a Webhook Relay endpoint. 2. A transformation function parses the payload and builds a HubSpot API request. 3. Webhook Relay forwards it to HubSpot, and the record is created. ## [Step 1: Get your HubSpot credentials](#step-1-get-your-hubspot-credentials) You need a **private app** access token (HubSpot → Settings → Integrations → Private Apps) with CRM write scopes. ## [Step 2: Create a Webhook Relay output to the HubSpot API](#step-2-create-a-webhook-relay-output-to-the-hubspot-api) Create a bucket with a public input, then add an output pointing at the HubSpot API: - **Output destination:** `https://api.hubapi.com/crm/v3/objects/contacts` - **Headers:** - `Authorization: Bearer ` - `Content-Type: application/json` ## [Step 3: Add a transformation function](#step-3-add-a-transformation-function) Attach a function that reshapes the incoming webhook. HubSpot's CRM API expects a `properties` object whose keys are HubSpot contact properties (`email`, `firstname`, `lastname`, `phone`). ``` -- incoming payload is in r.RequestBody local body = json.decode(r.RequestBody) local contact = { properties = { email = body.email, firstname = body.first_name or body.name, lastname = body.last_name } } r:SetRequestBody(json.encode(contact)) ``` ## [Step 4: Point your source at the URL and test](#step-4-point-your-source-at-the-url-and-test) Configure your source service's webhook to point at the Webhook Relay public URL. Trigger an event — or replay one from the [Webhook Bin](https://webhookrelay.com/blog/webhook-to-hubspot/webhook-bin/) — and the record appears in HubSpot within seconds. If the API rejects the request, the Webhook Relay logs show HubSpot's exact error so you can fix the mapping. ## [Going further](#going-further) - **Fan out:** send the same event to [Salesforce](https://webhookrelay.com/blog/webhook-to-hubspot/blog/webhook-to-salesforce/) or [Slack](https://webhookrelay.com/blog/webhook-to-hubspot/blog/webhook-to-slack/) at the same time with [multiple destinations](https://webhookrelay.com/blog/webhook-to-hubspot/features/webhook-multiple-destinations/). - **Filter:** use [forwarding rules](https://webhookrelay.com/blog/webhook-to-hubspot/features/forwarding-rules/) so only the events you care about reach HubSpot. - **Inspect first:** see the raw payload in the [Webhook Bin](https://webhookrelay.com/blog/webhook-to-hubspot/webhook-bin/) before writing the mapping. ## [Get started](#get-started) [Create a free Webhook Relay account](https://my.webhookrelay.com/register) and turn any webhook into HubSpot records — no servers to run. New to webhooks? Start with [what is a webhook](https://webhookrelay.com/blog/webhook-to-hubspot/blog/what-is-webhook/) and [how to transform webhooks](https://webhookrelay.com/blog/webhook-to-hubspot/features/transform-webhooks/). --- --- title: Send a Webhook to Salesforce | WebhookRelay meta: "og: title": "Send a Webhook to Salesforce" description: Send any webhook to Salesforce. Transform an incoming webhook into the Salesforce API format in flight and forward it — no glue server, no code to maintain. url: https://webhookrelay.com/blog/webhook-to-salesforce.md file: /blog/webhook-to-salesforce.md --- ![Stripes](https://webhookrelay.com/blog/webhook-to-salesforce/images/stripes.svg) # **Send a Webhook to Salesforce** Send any webhook to Salesforce. Transform an incoming webhook into the Salesforce API format in flight and forward it — no glue server, no code to maintain. ![Send a Webhook to Salesforce: Transform & Forward Any Payload](https://webhookrelay.com/blog/webhook-to-salesforce/images/blog/heroes/route.jpg) You have a service that fires webhooks — a form, a payment provider, a CI pipeline, a monitoring tool — and you want each event to create a record (a Lead, Contact or custom object) in **Salesforce**. The problem: the Salesforce API won't accept the raw webhook. It expects its own JSON shape and authentication, and the payload your source sends never matches. [Webhook Relay](https://webhookrelay.com) sits in the middle. It receives the incoming webhook at a stable public URL, **transforms** the payload into the format Salesforce expects, and delivers it — no glue server, no Lambda, no maintenance. ## [How it works](#how-it-works) 1. Your source service POSTs its webhook to a Webhook Relay endpoint. 2. A transformation function parses the payload and builds a Salesforce API request. 3. Webhook Relay forwards it to Salesforce, and the record is created. ## [Step 1: Get your Salesforce credentials](#step-1-get-your-salesforce-credentials) You need an OAuth **access token** from a Connected App (Salesforce → Setup → App Manager). ## [Step 2: Create a Webhook Relay output to the Salesforce API](#step-2-create-a-webhook-relay-output-to-the-salesforce-api) Create a bucket with a public input, then add an output pointing at the Salesforce API: - **Output destination:** `https://YOUR_INSTANCE.salesforce.com/services/data/v60.0/sobjects/Lead` - **Headers:** - `Authorization: Bearer ` - `Content-Type: application/json` ## [Step 3: Add a transformation function](#step-3-add-a-transformation-function) Attach a function that reshapes the incoming webhook. Salesforce's REST API expects a JSON object whose keys are the sObject's field API names (`LastName`, `Company`, `Email` for a Lead). ``` -- incoming payload is in r.RequestBody local body = json.decode(r.RequestBody) local lead = { LastName = body.last_name or body.name or "Unknown", Company = body.company or "Unknown", Email = body.email } r:SetRequestBody(json.encode(lead)) ``` ## [Step 4: Point your source at the URL and test](#step-4-point-your-source-at-the-url-and-test) Configure your source service's webhook to point at the Webhook Relay public URL. Trigger an event — or replay one from the [Webhook Bin](https://webhookrelay.com/blog/webhook-to-salesforce/webhook-bin/) — and the record appears in Salesforce within seconds. If the API rejects the request, the Webhook Relay logs show Salesforce's exact error so you can fix the mapping. ## [Going further](#going-further) - **Fan out:** send the same event to [HubSpot](https://webhookrelay.com/blog/webhook-to-salesforce/blog/webhook-to-hubspot/) or [Email](https://webhookrelay.com/blog/webhook-to-salesforce/blog/webhook-to-email/) at the same time with [multiple destinations](https://webhookrelay.com/blog/webhook-to-salesforce/features/webhook-multiple-destinations/). - **Filter:** use [forwarding rules](https://webhookrelay.com/blog/webhook-to-salesforce/features/forwarding-rules/) so only the events you care about reach Salesforce. - **Inspect first:** see the raw payload in the [Webhook Bin](https://webhookrelay.com/blog/webhook-to-salesforce/webhook-bin/) before writing the mapping. ## [Get started](#get-started) [Create a free Webhook Relay account](https://my.webhookrelay.com/register) and turn any webhook into Salesforce records — no servers to run. New to webhooks? Start with [what is a webhook](https://webhookrelay.com/blog/webhook-to-salesforce/blog/what-is-webhook/) and [how to transform webhooks](https://webhookrelay.com/blog/webhook-to-salesforce/features/transform-webhooks/). --- --- title: Send a Webhook to Trello | WebhookRelay meta: "og: title": "Send a Webhook to Trello" description: Send any webhook to Trello. Transform an incoming webhook into the Trello API format in flight and forward it — no glue server, no code to maintain. url: https://webhookrelay.com/blog/webhook-to-trello.md file: /blog/webhook-to-trello.md --- ![Stripes](https://webhookrelay.com/blog/webhook-to-trello/images/stripes.svg) # **Send a Webhook to Trello** Send any webhook to Trello. Transform an incoming webhook into the Trello API format in flight and forward it — no glue server, no code to maintain. ![Send a Webhook to Trello: Transform & Forward Any Payload](https://webhookrelay.com/blog/webhook-to-trello/images/blog/heroes/route.jpg) You have a service that fires webhooks — a form, a payment provider, a CI pipeline, a monitoring tool — and you want each event to create a card in **Trello**. The problem: the Trello API won't accept the raw webhook. It expects its own JSON shape and authentication, and the payload your source sends never matches. [Webhook Relay](https://webhookrelay.com) sits in the middle. It receives the incoming webhook at a stable public URL, **transforms** the payload into the format Trello expects, and delivers it — no glue server, no Lambda, no maintenance. ## [How it works](#how-it-works) 1. Your source service POSTs its webhook to a Webhook Relay endpoint. 2. A transformation function parses the payload and builds a Trello API request. 3. Webhook Relay forwards it to Trello, and the record is created. ## [Step 1: Get your Trello credentials](#step-1-get-your-trello-credentials) You need a Trello **API key** and **token** (trello.com/app-key), and the **list ID** to add cards to. ## [Step 2: Create a Webhook Relay output to the Trello API](#step-2-create-a-webhook-relay-output-to-the-trello-api) Create a bucket with a public input, then add an output pointing at the Trello API: - **Output destination:** `https://api.trello.com/1/cards?key=KEY&token=TOKEN&idList=LIST_ID` — Trello takes the key, token and target list as **query parameters**, not headers. - **Headers:** `Content-Type: application/json` ## [Step 3: Add a transformation function](#step-3-add-a-transformation-function) Attach a function that reshapes the incoming webhook. Trello's create-card endpoint takes the `key`, `token` and `idList` as query parameters and the card `name`/`desc` in the body. ``` -- incoming payload is in r.RequestBody local body = json.decode(r.RequestBody) local card = { name = body.title or "Card from webhook", desc = body.message or r.RequestBody } r:SetRequestBody(json.encode(card)) ``` ## [Step 4: Point your source at the URL and test](#step-4-point-your-source-at-the-url-and-test) Configure your source service's webhook to point at the Webhook Relay public URL. Trigger an event — or replay one from the [Webhook Bin](https://webhookrelay.com/blog/webhook-to-trello/webhook-bin/) — and the record appears in Trello within seconds. If the API rejects the request, the Webhook Relay logs show Trello's exact error so you can fix the mapping. ## [Going further](#going-further) - **Fan out:** send the same event to [Jira](https://webhookrelay.com/blog/webhook-to-trello/blog/webhook-to-jira/) or [Discord](https://webhookrelay.com/blog/webhook-to-trello/blog/webhook-to-discord/) at the same time with [multiple destinations](https://webhookrelay.com/blog/webhook-to-trello/features/webhook-multiple-destinations/). - **Filter:** use [forwarding rules](https://webhookrelay.com/blog/webhook-to-trello/features/forwarding-rules/) so only the events you care about reach Trello. - **Inspect first:** see the raw payload in the [Webhook Bin](https://webhookrelay.com/blog/webhook-to-trello/webhook-bin/) before writing the mapping. ## [Get started](#get-started) [Create a free Webhook Relay account](https://my.webhookrelay.com/register) and turn any webhook into Trello records — no servers to run. New to webhooks? Start with [what is a webhook](https://webhookrelay.com/blog/webhook-to-trello/blog/what-is-webhook/) and [how to transform webhooks](https://webhookrelay.com/blog/webhook-to-trello/features/transform-webhooks/). --- --- title: Send a Webhook to Jira | WebhookRelay meta: "og: title": "Send a Webhook to Jira" description: Send any webhook to Jira. Transform an incoming webhook into the Jira API format in flight and forward it — no glue server, no code to maintain. url: https://webhookrelay.com/blog/webhook-to-jira.md file: /blog/webhook-to-jira.md --- ![Stripes](https://webhookrelay.com/blog/webhook-to-jira/images/stripes.svg) # **Send a Webhook to Jira** Send any webhook to Jira. Transform an incoming webhook into the Jira API format in flight and forward it — no glue server, no code to maintain. ![Send a Webhook to Jira: Transform & Forward Any Payload](https://webhookrelay.com/blog/webhook-to-jira/images/blog/heroes/route.jpg) You have a service that fires webhooks — a form, a payment provider, a CI pipeline, a monitoring tool — and you want each event to create an issue in **Jira**. The problem: the Jira API won't accept the raw webhook. It expects its own JSON shape and authentication, and the payload your source sends never matches. [Webhook Relay](https://webhookrelay.com) sits in the middle. It receives the incoming webhook at a stable public URL, **transforms** the payload into the format Jira expects, and delivers it — no glue server, no Lambda, no maintenance. ## [How it works](#how-it-works) 1. Your source service POSTs its webhook to a Webhook Relay endpoint. 2. A transformation function parses the payload and builds a Jira API request. 3. Webhook Relay forwards it to Jira, and the record is created. ## [Step 1: Get your Jira credentials](#step-1-get-your-jira-credentials) You need a Jira **API token** (id.atlassian.com) paired with your account email, sent as Basic auth. ## [Step 2: Create a Webhook Relay output to the Jira API](#step-2-create-a-webhook-relay-output-to-the-jira-api) Create a bucket with a public input, then add an output pointing at the Jira API: - **Output destination:** `https://YOUR_SITE.atlassian.net/rest/api/3/issue` - **Headers:** - `Authorization: Basic ` - `Content-Type: application/json` ## [Step 3: Add a transformation function](#step-3-add-a-transformation-function) Attach a function that reshapes the incoming webhook. Jira's REST API expects a `fields` object with a `project` (by key), an `issuetype` (by name), a `summary`, and a `description` (Atlassian Document Format). ``` -- incoming payload is in r.RequestBody local body = json.decode(r.RequestBody) local issue = { fields = { project = { key = "ENG" }, issuetype = { name = "Task" }, summary = body.title or "Issue from webhook", description = { type = "doc", version = 1, content = {{ type = "paragraph", content = {{ type = "text", text = body.message or "" }} }} } } } r:SetRequestBody(json.encode(issue)) ``` ## [Step 4: Point your source at the URL and test](#step-4-point-your-source-at-the-url-and-test) Configure your source service's webhook to point at the Webhook Relay public URL. Trigger an event — or replay one from the [Webhook Bin](https://webhookrelay.com/blog/webhook-to-jira/webhook-bin/) — and the record appears in Jira within seconds. If the API rejects the request, the Webhook Relay logs show Jira's exact error so you can fix the mapping. ## [Going further](#going-further) - **Fan out:** send the same event to [Trello](https://webhookrelay.com/blog/webhook-to-jira/blog/webhook-to-trello/) or [Slack](https://webhookrelay.com/blog/webhook-to-jira/blog/webhook-to-slack/) at the same time with [multiple destinations](https://webhookrelay.com/blog/webhook-to-jira/features/webhook-multiple-destinations/). - **Filter:** use [forwarding rules](https://webhookrelay.com/blog/webhook-to-jira/features/forwarding-rules/) so only the events you care about reach Jira. - **Inspect first:** see the raw payload in the [Webhook Bin](https://webhookrelay.com/blog/webhook-to-jira/webhook-bin/) before writing the mapping. ## [Get started](#get-started) [Create a free Webhook Relay account](https://my.webhookrelay.com/register) and turn any webhook into Jira records — no servers to run. New to webhooks? Start with [what is a webhook](https://webhookrelay.com/blog/webhook-to-jira/blog/what-is-webhook/) and [how to transform webhooks](https://webhookrelay.com/blog/webhook-to-jira/features/transform-webhooks/). --- --- title: How to Send Webhooks to AWS SQS (No Consumer Code) meta: "og: title": "How to Send Webhooks to AWS SQS (No Consumer Code)" description: Forward incoming webhooks straight into an Amazon SQS queue with Service Connections — no Lambda and no consumer to write. Decouple and buffer safely. url: https://webhookrelay.com/blog/webhook-to-sqs.md file: /blog/webhook-to-sqs.md --- ![Stripes](https://webhookrelay.com/blog/webhook-to-sqs/images/stripes.svg) # **How to Send Webhooks to AWS SQS (No Consumer Code)** Forward incoming webhooks straight into an Amazon SQS queue with Service Connections — no Lambda and no consumer to write. Decouple and buffer safely. ![How to Send Webhooks to AWS SQS (No Consumer Code)](https://webhookrelay.com/blog/webhook-to-sqs/images/blog/heroes/route.jpg) Putting webhooks onto a queue is a common reliability pattern: accept the event fast, drop it on a queue, and process it asynchronously so a slow handler never causes the provider to retry or time out. The usual way to get there is to stand up an API Gateway + Lambda just to call `SendMessage`. With Webhook Relay [Service Connections](https://webhookrelay.com/blog/webhook-to-sqs/blog/introducing_service_connections/), you skip that — webhooks land in [Amazon SQS](https://aws.amazon.com/sqs/) directly. ## [How it works](#how-it-works) Webhook Relay has **service connections** (your cloud credentials) and **outputs** (where to send data). When a webhook arrives in a bucket, Webhook Relay forwards it to every output attached to that bucket — including an SQS queue: ``` Provider --> Webhook Relay bucket --> SQS output --> your queue --> your consumer ``` No public Lambda, no consumer needed just to enqueue. Your own worker reads the queue on its own schedule. ## [1. Create an AWS service connection](#_1-create-an-aws-service-connection) In the dashboard, add a **service connection** for AWS with an access key and secret. The key only needs one permission for this: ``` { "Effect": "Allow", "Action": "sqs:SendMessage", "Resource": "arn:aws:sqs:us-east-1:123456789012:my-queue" } ``` Secrets are encrypted at rest (AES-256-GCM) and never returned in full by the API. ## [2. Attach an SQS output to a bucket](#_2-attach-an-sqs-output-to-a-bucket) Add an **SQS output** to the bucket that will receive your webhooks. The only required field is the full queue URL: ``` https://sqs.us-east-1.amazonaws.com/123456789012/my-queue ``` The region is auto-detected from the URL (an `arn:aws:sqs:...` ARN works too). From now on, every webhook delivered to the bucket is sent to the queue as a message. ## [3. Point your provider at the bucket URL](#_3-point-your-provider-at-the-bucket-url) Use the bucket's public Webhook Relay endpoint as the webhook URL in Stripe, GitHub, Shopify, or any provider. Test it with `curl`: ``` curl -X POST https://your-webhook-relay-endpoint \ -H 'Content-Type: application/json' \ -d '{"event":"order.created","id":"ord_123"}' ``` The message appears in your SQS queue within seconds. ## [Why route webhooks through a queue](#why-route-webhooks-through-a-queue) - **Absorb spikes.** A burst of webhooks queues up instead of overwhelming your handler. - **Decouple delivery from processing.** Your consumer can be down for maintenance; messages wait in SQS. - **Fan-in.** Send webhooks from many providers into one queue and process them uniformly. - **Retry safely.** Combine with SQS visibility timeouts and a dead-letter queue. ## [Going further](#going-further) - Add a [transformation](https://webhookrelay.com/blog/webhook-to-sqs/features/transform-webhooks/) to reshape or enrich the payload before it hits the queue. - [Filter](https://webhookrelay.com/blog/webhook-to-sqs/features/forwarding-rules/) so only the events you care about are enqueued. - Send to [multiple destinations](https://webhookrelay.com/blog/webhook-to-sqs/features/webhook-multiple-destinations/) at once — e.g. SQS for processing and S3 for an archive. - Prefer Google Cloud? See [sending webhooks to Pub/Sub](https://webhookrelay.com/blog/webhook-to-sqs/blog/webhook-to-pubsub/). [Inspect a webhook first](https://webhookrelay.com/blog/webhook-to-sqs/webhook-bin/) or [create a free account](https://my.webhookrelay.com/register) to wire up the SQS output. --- --- title: Send a Webhook to Airtable | WebhookRelay meta: "og: title": "Send a Webhook to Airtable" description: Send any webhook to Airtable. Forward incoming webhooks into an Airtable base, transforming the payload into the Airtable API format in flight. url: https://webhookrelay.com/blog/webhook-to-airtable.md file: /blog/webhook-to-airtable.md --- ![Stripes](https://webhookrelay.com/blog/webhook-to-airtable/images/stripes.svg) # **Send a Webhook to Airtable** Send any webhook to Airtable. Forward incoming webhooks into an Airtable base, transforming the payload into the Airtable API format in flight. ![Send a Webhook to Airtable: Transform & Forward Any Payload](https://webhookrelay.com/blog/webhook-to-airtable/images/blog/heroes/route.jpg) You have a service that fires webhooks — a signup form, a payment provider, an e-commerce platform, a monitoring tool — and you want each event to land as a record in an **Airtable base**. The problem: the Airtable API won't accept the raw webhook. It expects a bearer token and a `fields` object whose keys match your table exactly, and the payload your source sends never looks like that. [Webhook Relay](https://webhookrelay.com) sits in the middle. It receives the incoming webhook at a stable public URL, **transforms** the payload into the Airtable "create records" format, and delivers it to the Airtable API — no glue server, no Zapier task limits, no maintenance. ## [How it works](#how-it-works) 1. Your source service POSTs its webhook to a Webhook Relay endpoint. 2. A transformation function parses the payload and builds an Airtable API request body. 3. Webhook Relay forwards that to the Airtable API with your token, and a new record appears in your table. ## [Step 1: Create an Airtable token and find your IDs](#step-1-create-an-airtable-token-and-find-your-ids) 1. Go to [airtable.com/create/tokens](https://airtable.com/create/tokens) and create a **personal access token** with the `data.records:write` scope and access to your base. 2. Find your **base ID** (`app...`) and **table name** from the base's API docs page. ## [Step 2: Create a Webhook Relay output to the Airtable API](#step-2-create-a-webhook-relay-output-to-the-airtable-api) Create a bucket with a public input, then add an output pointing at the Airtable API: - **Output destination:** `https://api.airtable.com/v0//` - **Headers:** - `Authorization: Bearer ` - `Content-Type: application/json` ## [Step 3: Add a transformation function](#step-3-add-a-transformation-function) Attach a function to the output that reshapes the incoming webhook into an Airtable record. Map the fields you care about to your table's column names: ``` -- incoming payload is in r.RequestBody local body = json.decode(r.RequestBody) local payload = { records = { { fields = { Name = body.name or "New event", Email = body.email or "", Amount = body.amount or 0 } } } } r:SetRequestBody(json.encode(payload)) ``` Each key under `fields` must match a column in your Airtable table and use a compatible value type. ## [Step 4: Point your source at the URL and test](#step-4-point-your-source-at-the-url-and-test) Configure your source service's webhook to point at your Webhook Relay public URL. Trigger an event — or replay one from the [Webhook Bin](https://webhookrelay.com/blog/webhook-to-airtable/webhook-bin/) — and a new record appears in Airtable within seconds. If Airtable rejects the request, the Webhook Relay logs show the exact error so you can fix the field mapping. ## [Going further](#going-further) - **Fan out:** send the same event to [Slack](https://webhookrelay.com/blog/webhook-to-airtable/blog/webhook-to-slack/), [Notion](https://webhookrelay.com/blog/webhook-to-airtable/blog/webhook-to-notion/) or a [Google Sheet](https://webhookrelay.com/blog/webhook-to-airtable/blog/webhook-to-google-sheets/) at the same time with [multiple destinations](https://webhookrelay.com/blog/webhook-to-airtable/features/webhook-multiple-destinations/). - **Test locally first:** inspect the real payload in the [Webhook Bin](https://webhookrelay.com/blog/webhook-to-airtable/webhook-bin/) before you write the mapping. - **Secure it:** [verify the source signature](https://webhookrelay.com/blog/webhook-to-airtable/blog/verify-webhook-signature/) before forwarding so only legitimate events reach Airtable. ## [Get started](#get-started) [Create a free Webhook Relay account](https://my.webhookrelay.com/register) and turn any webhook into Airtable records — no servers to run. New to webhooks? Start with [what is a webhook](https://webhookrelay.com/blog/webhook-to-airtable/blog/what-is-webhook/) and [how to transform webhooks](https://webhookrelay.com/blog/webhook-to-airtable/features/transform-webhooks/). --- --- title: Send a Webhook to Notion | WebhookRelay meta: "og: title": "Send a Webhook to Notion" description: Send any webhook to Notion. Forward incoming webhooks into a Notion database, transforming the payload into the Notion API format in flight. url: https://webhookrelay.com/blog/webhook-to-notion.md file: /blog/webhook-to-notion.md --- ![Stripes](https://webhookrelay.com/blog/webhook-to-notion/images/stripes.svg) # **Send a Webhook to Notion** Send any webhook to Notion. Forward incoming webhooks into a Notion database, transforming the payload into the Notion API format in flight. ![Send a Webhook to Notion: Transform & Forward Any Payload](https://webhookrelay.com/blog/webhook-to-notion/images/blog/heroes/route.jpg) You have a service that fires webhooks — a signup form, a payment provider, a CRM, a monitoring tool — and you want each event to land as a row in a **Notion database**. The problem: the Notion API won't accept the raw webhook. It expects a bearer token, a `Notion-Version` header and a `properties` object that matches your database schema exactly, and the payload your source sends never looks like that. [Webhook Relay](https://webhookrelay.com) sits in the middle. It receives the incoming webhook at a stable public URL, **transforms** the payload into the Notion "create a page" format, and delivers it to the Notion API — no glue server, no Lambda, no maintenance. ## [How it works](#how-it-works) 1. Your source service POSTs its webhook to a Webhook Relay endpoint. 2. A transformation function parses the payload and builds a Notion API request body. 3. Webhook Relay forwards that to `https://api.notion.com/v1/pages` with your integration token, and a new row appears in your database. ## [Step 1: Create a Notion integration and share your database](#step-1-create-a-notion-integration-and-share-your-database) 1. Go to [notion.so/my-integrations](https://www.notion.so/my-integrations) and create a new **internal integration**. Copy its secret (it starts with `secret_` or `ntn_`). 2. Open the database you want to write to, click the **•••** menu → **Connections** → add your integration so it has access. 3. Note the **database ID** — it's the 32-character string in the database URL. ## [Step 2: Create a Webhook Relay output to the Notion API](#step-2-create-a-webhook-relay-output-to-the-notion-api) Create a bucket with a public input, then add an output pointing at the Notion API: - **Output destination:** `https://api.notion.com/v1/pages` - **Headers:** - `Authorization: Bearer ` - `Notion-Version: 2022-06-28` - `Content-Type: application/json` ## [Step 3: Add a transformation function](#step-3-add-a-transformation-function) Attach a function to the output that reshapes the incoming webhook into a Notion page. Map the fields you care about to your database's property names: ``` -- incoming payload is in r.RequestBody local body = json.decode(r.RequestBody) local page = { parent = { database_id = "YOUR_DATABASE_ID" }, properties = { Name = { title = { { text = { content = body.name or "New event" } } } }, Email = { rich_text = { { text = { content = body.email or "" } } } }, Amount = { number = body.amount or 0 } } } r:SetRequestBody(json.encode(page)) ``` Make sure each key under `properties` matches a column in your Notion database and uses the correct type (`title`, `rich_text`, `number`, `select`, `date`, `checkbox`). ## [Step 4: Point your source at the URL and test](#step-4-point-your-source-at-the-url-and-test) Configure your source service's webhook to point at your Webhook Relay public URL. Trigger an event — or replay one from the [Webhook Bin](https://webhookrelay.com/blog/webhook-to-notion/webhook-bin/) — and a new row appears in Notion within seconds. If the API rejects the request, the Webhook Relay logs show Notion's exact error so you can fix the property mapping. ## [Going further](#going-further) - **Fan out:** send the same event to [Slack](https://webhookrelay.com/blog/webhook-to-notion/blog/webhook-to-slack/), [Discord](https://webhookrelay.com/blog/webhook-to-notion/blog/webhook-to-discord/) or a [Google Sheet](https://webhookrelay.com/blog/webhook-to-notion/blog/webhook-to-google-sheets/) at the same time with [multiple destinations](https://webhookrelay.com/blog/webhook-to-notion/features/webhook-multiple-destinations/). - **Test locally first:** [receive the webhook on localhost](https://webhookrelay.com/blog/webhook-to-notion/blog/what-is-webhook/) to inspect the real payload before you write the mapping. - **Secure it:** [verify the source signature](https://webhookrelay.com/blog/webhook-to-notion/blog/verify-webhook-signature/) before forwarding so only legitimate events reach Notion. ## [Get started](#get-started) [Create a free Webhook Relay account](https://my.webhookrelay.com/register) and turn any webhook into Notion rows — no servers to run. New to webhooks? Start with [what is a webhook](https://webhookrelay.com/blog/webhook-to-notion/blog/what-is-webhook/) and [how to transform webhooks](https://webhookrelay.com/blog/webhook-to-notion/features/transform-webhooks/). --- --- title: How to Send Webhooks to Google Cloud Pub/Sub | WebhookRelay meta: "og: title": "How to Send Webhooks to Google Cloud Pub/Sub" description: Publish incoming webhooks straight to a Google Cloud Pub/Sub topic with Service Connections — no Cloud Function to write. Build event-driven pipelines. url: https://webhookrelay.com/blog/webhook-to-pubsub.md file: /blog/webhook-to-pubsub.md --- ![Stripes](https://webhookrelay.com/blog/webhook-to-pubsub/images/stripes.svg) # **How to Send Webhooks to Google Cloud Pub/Sub** Publish incoming webhooks straight to a Google Cloud Pub/Sub topic with Service Connections — no Cloud Function to write. Build event-driven pipelines. ![How to Send Webhooks to Google Cloud Pub/Sub](https://webhookrelay.com/blog/webhook-to-pubsub/images/blog/heroes/route.jpg) [Google Cloud Pub/Sub](https://cloud.google.com/pubsub) is the backbone of a lot of event-driven systems on GCP — publish a message, and any number of subscribers (Cloud Functions, Dataflow, BigQuery, your own services) react to it. The friction is getting an external **webhook** onto a topic in the first place, which usually means writing a Cloud Function just to call `publish`. Webhook Relay [Service Connections](https://webhookrelay.com/blog/webhook-to-pubsub/blog/introducing_service_connections/) let webhooks publish to Pub/Sub directly. ## [How it works](#how-it-works) Webhook Relay has **service connections** (your cloud credentials) and **outputs** (where to send data). Attach a Pub/Sub output to a bucket, and every webhook that arrives is published to your topic: ``` Provider --> Webhook Relay bucket --> Pub/Sub output --> your topic --> subscribers ``` No HTTP-triggered Cloud Function in the middle. Your existing subscribers do the work. ## [1. Create a GCP service connection](#_1-create-a-gcp-service-connection) Create a service account in your GCP project, grant it the **Pub/Sub Publisher** role (`roles/pubsub.publisher`) on the target topic, and create a JSON key. In Webhook Relay, add a **service connection** for GCP with your project ID and the full JSON key contents. The key is encrypted at rest and never returned in full by the API. ## [2. Create the topic and attach a Pub/Sub output](#_2-create-the-topic-and-attach-a-pubsub-output) Make sure the topic exists (Webhook Relay publishes to it but doesn't create it): ``` gcloud pubsub topics create webhooks ``` Then add a **Pub/Sub output** to your bucket with the topic name (`webhooks`). From now on, every webhook delivered to the bucket is published to that topic, with the request body as the message data and headers carried as attributes. ## [3. Point your provider at the bucket URL](#_3-point-your-provider-at-the-bucket-url) Use the bucket's public Webhook Relay endpoint as the webhook URL in your provider, and test it: ``` curl -X POST https://your-webhook-relay-endpoint \ -H 'Content-Type: application/json' \ -d '{"event":"build.finished","status":"success"}' ``` A message lands on your Pub/Sub topic, and your subscribers fire. ## [Why publish webhooks to Pub/Sub](#why-publish-webhooks-to-pubsub) - **Decouple producers from consumers.** Webhooks become events many services can subscribe to independently. - **Scale and buffer.** Pub/Sub absorbs spikes and delivers at the rate your subscribers can handle. - **Fan-out on GCP.** One webhook → many subscribers (a Cloud Function, a BigQuery pipeline, an alerting service). ## [Going further](#going-further) - Add a [transformation](https://webhookrelay.com/blog/webhook-to-pubsub/features/transform-webhooks/) to reshape the payload before publishing. - [Filter](https://webhookrelay.com/blog/webhook-to-pubsub/features/forwarding-rules/) so only relevant events are published. - Deliver to [multiple destinations](https://webhookrelay.com/blog/webhook-to-pubsub/features/webhook-multiple-destinations/) at once. - On AWS instead? See [sending webhooks to SQS](https://webhookrelay.com/blog/webhook-to-pubsub/blog/webhook-to-sqs/). [Inspect a webhook first](https://webhookrelay.com/blog/webhook-to-pubsub/webhook-bin/) or [create a free account](https://my.webhookrelay.com/register) to set up the Pub/Sub output. --- --- title: Send a Webhook to BigQuery: Stream Events Into a Table meta: "og: title": "Send a Webhook to BigQuery: Stream Events Into a Table" description: Stream incoming webhooks straight into Google BigQuery. Insert each event as a row for real-time analytics — no pipeline to build or maintain. url: https://webhookrelay.com/blog/webhook-to-bigquery.md file: /blog/webhook-to-bigquery.md --- ![Stripes](https://webhookrelay.com/blog/webhook-to-bigquery/images/stripes.svg) # **Send a Webhook to BigQuery: Stream Events Into a Table** Stream incoming webhooks straight into Google BigQuery. Insert each event as a row for real-time analytics — no pipeline to build or maintain. ![Send a Webhook to BigQuery: Stream Events Into a Table](https://webhookrelay.com/blog/webhook-to-bigquery/images/blog/heroes/route.jpg) You want every webhook — payments, signups, deploys, alerts — to land in [Google BigQuery](https://cloud.google.com/bigquery) as a row so you can query and dashboard it. The usual way is to stand up Pub/Sub plus a Dataflow job or a small server to receive the webhook and call the BigQuery API. That's a lot of moving parts for "insert a row." [Webhook Relay](https://webhookrelay.com) does it with a **function**: it receives the webhook at a stable public URL and inserts the event straight into BigQuery using the streaming insert API — no pipeline, no server. ## [How it works](#how-it-works) Webhook Relay runs a small function on each incoming webhook. The built-in BigQuery package authenticates with a service-account key and streams rows into your table: ``` Provider --> Webhook Relay bucket --> BigQuery function --> your_dataset.your_table ``` It runs alongside normal delivery, so you can forward the webhook to your app **and** insert it into BigQuery at the same time. ## [1. Prepare BigQuery](#_1-prepare-bigquery) 1. Create a dataset and a table whose schema matches the fields you want to store (e.g. `event STRING`, `id STRING`, `amount NUMERIC`, `received_at TIMESTAMP`). 2. Create a **service account** with the `roles/bigquery.dataEditor` role and download its JSON key. ## [2. Add the BigQuery function](#_2-add-the-bigquery-function) Create a bucket with a public input and attach a function that maps the payload to a row and inserts it. The full, copy-pasteable example is in the docs: [insert and stream data into BigQuery](https://webhookrelay.com/blog/webhook-to-bigquery/docs/tutorials/warehouse/bigquery/) and the [BigQuery functions reference](https://webhookrelay.com/blog/webhook-to-bigquery/docs/webhooks/functions/big-query/). ``` local body = json.decode(r.RequestBody) local row = { event = body.event, id = body.id, amount = body.amount, received_at = os.date("!%Y-%m-%dT%H:%M:%SZ") } bigquery.insert("your-project", "your_dataset", "your_table", row) ``` ## [3. Point your provider at the URL and test](#_3-point-your-provider-at-the-url-and-test) Use the bucket's public Webhook Relay endpoint as the webhook URL in your provider, then fire a test event: ``` curl -X POST https://your-webhook-relay-endpoint \ -H 'Content-Type: application/json' \ -d '{"event":"invoice.paid","id":"in_123","amount":19.99}' ``` Query your table a few seconds later and the row is there. ## [Why stream webhooks into BigQuery](#why-stream-webhooks-into-bigquery) - **Real-time analytics.** Dashboard payments, signups or deploys without waiting for a nightly batch. - **No pipeline to maintain.** Skip Pub/Sub + Dataflow for simple inserts. - **One source, many uses.** Forward to your app, archive to [GCS](https://webhookrelay.com/blog/webhook-to-bigquery/blog/webhook-to-gcs/), and stream to BigQuery from the same webhook. ## [Going further](#going-further) - Archive the raw payload to [Google Cloud Storage](https://webhookrelay.com/blog/webhook-to-bigquery/blog/webhook-to-gcs/) or [Amazon S3](https://webhookrelay.com/blog/webhook-to-bigquery/blog/webhook-to-s3/) alongside the insert. - [Transform](https://webhookrelay.com/blog/webhook-to-bigquery/features/transform-webhooks/) or [filter](https://webhookrelay.com/blog/webhook-to-bigquery/features/forwarding-rules/) events before they reach the table. - Fan a single event out to [multiple destinations](https://webhookrelay.com/blog/webhook-to-bigquery/features/webhook-multiple-destinations/). [Inspect a webhook first](https://webhookrelay.com/blog/webhook-to-bigquery/webhook-bin/) or [create a free account](https://my.webhookrelay.com/register) to start streaming into BigQuery. --- --- title: How to Archive Webhooks to Amazon S3 (Store Every Payload) meta: "og: title": "How to Archive Webhooks to Amazon S3 (Store Every Payload)" description: Store every incoming webhook in Amazon S3 with Service Connections — no code. Keep an auditable archive of payloads for compliance and debugging. url: https://webhookrelay.com/blog/webhook-to-s3.md file: /blog/webhook-to-s3.md --- ![Stripes](https://webhookrelay.com/blog/webhook-to-s3/images/stripes.svg) # **How to Archive Webhooks to Amazon S3 (Store Every Payload)** Store every incoming webhook in Amazon S3 with Service Connections — no code. Keep an auditable archive of payloads for compliance and debugging. ![How to Archive Webhooks to Amazon S3 (Store Every Payload)](https://webhookrelay.com/blog/webhook-to-s3/images/blog/heroes/route.jpg) When a webhook drives something important — a payment, an order, a deploy — you eventually want a record of **exactly what the provider sent**, not just whether your handler succeeded. An archive in [Amazon S3](https://aws.amazon.com/s3/) gives you that: an auditable, replayable history of every payload. Webhook Relay [Service Connections](https://webhookrelay.com/blog/webhook-to-s3/blog/introducing_service_connections/) write webhooks to S3 for you, with no code. ## [How it works](#how-it-works) Webhook Relay has **service connections** (your cloud credentials) and **outputs** (where to send data). Attach an S3 output to a bucket, and every webhook that arrives is stored as an object: ``` Provider --> Webhook Relay bucket --> S3 output --> s3://your-bucket/... ``` This runs alongside your normal delivery — you can forward the webhook to your app **and** archive it to S3 at the same time. ## [1. Create an AWS service connection](#_1-create-an-aws-service-connection) Add a **service connection** for AWS with an access key whose only required permission is: ``` { "Effect": "Allow", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::my-bucket/*" } ``` Secrets are encrypted at rest (AES-256-GCM) and never returned in full by the API. ## [2. Attach an S3 output](#_2-attach-an-s3-output) Add an **S3 output** to the bucket that receives your webhooks: - **Bucket name** and **region** (required). - **Prefix** (optional) — e.g. `stripe/`. - **Format** — `json` (full webhook with headers, the default), `body_only` (raw body), or `har`. Objects are written under a dated key so they're easy to browse and lifecycle: ``` {prefix}/{year}/{month}/{day}/{log_id}.json ``` ## [3. Point your provider at the bucket URL](#_3-point-your-provider-at-the-bucket-url) Use the bucket's public Webhook Relay endpoint as the webhook URL in your provider, then test: ``` curl -X POST https://your-webhook-relay-endpoint \ -H 'Content-Type: application/json' \ -d '{"event":"invoice.paid","id":"in_123"}' ``` An object appears in S3 within seconds. ## [Why archive webhooks](#why-archive-webhooks) - **Audit & compliance.** Keep an immutable record of what each provider sent, with retention via S3 lifecycle rules. - **Debug after the fact.** Inspect the exact payload that caused a problem, even days later. - **Replay.** Re-feed stored payloads into a new service or a rebuilt handler. - **Analytics.** Point Athena or a data pipeline at the archive. ## [Going further](#going-further) - Forward to your app **and** archive at once with [multiple destinations](https://webhookrelay.com/blog/webhook-to-s3/features/webhook-multiple-destinations/). - [Transform](https://webhookrelay.com/blog/webhook-to-s3/features/transform-webhooks/) or [filter](https://webhookrelay.com/blog/webhook-to-s3/features/forwarding-rules/) before storing. - Need a queue or topic instead? See [webhooks to SQS](https://webhookrelay.com/blog/webhook-to-s3/blog/webhook-to-sqs/) and [webhooks to Pub/Sub](https://webhookrelay.com/blog/webhook-to-s3/blog/webhook-to-pubsub/). On Google Cloud, the same works with GCS. [Inspect a webhook first](https://webhookrelay.com/blog/webhook-to-s3/webhook-bin/) or [create a free account](https://my.webhookrelay.com/register) to set up the S3 archive. --- --- title: How to Archive Webhooks to Google Cloud Storage (GCS) meta: "og: title": "How to Archive Webhooks to Google Cloud Storage (GCS)" description: Store every incoming webhook in Google Cloud Storage with Service Connections — no code. Keep an auditable payload archive for compliance and replay. url: https://webhookrelay.com/blog/webhook-to-gcs.md file: /blog/webhook-to-gcs.md --- ![Stripes](https://webhookrelay.com/blog/webhook-to-gcs/images/stripes.svg) # **How to Archive Webhooks to Google Cloud Storage (GCS)** Store every incoming webhook in Google Cloud Storage with Service Connections — no code. Keep an auditable payload archive for compliance and replay. ![How to Archive Webhooks to Google Cloud Storage (GCS)](https://webhookrelay.com/blog/webhook-to-gcs/images/blog/heroes/route.jpg) When a webhook drives something important — a payment, an order, a deploy — you eventually want a record of **exactly what the provider sent**, not just whether your handler succeeded. An archive in [Google Cloud Storage](https://cloud.google.com/storage) gives you that: an auditable, replayable history of every payload, ready to load into BigQuery. Webhook Relay [Service Connections](https://webhookrelay.com/blog/webhook-to-gcs/docs/service-connections/) write webhooks to GCS for you, with no code. ## [How it works](#how-it-works) Webhook Relay has **service connections** (your cloud credentials) and **outputs** (where to send data). Attach a GCS output to a bucket, and every webhook that arrives is stored as an object: ``` Provider --> Webhook Relay bucket --> GCS output --> gs://your-bucket/... ``` This runs alongside your normal delivery — you can forward the webhook to your app **and** archive it to GCS at the same time. ## [1. Create a GCP service connection](#_1-create-a-gcp-service-connection) Add a **service connection** for GCP using a service account whose key can write objects to your bucket (the `roles/storage.objectCreator` role is enough). Webhook Relay encrypts secrets at rest (AES-256-GCM) and never returns them in full. See the [GCS service connection docs](https://webhookrelay.com/blog/webhook-to-gcs/docs/service-connections/gcp_gcs/) for the exact steps. ## [2. Attach a GCS output](#_2-attach-a-gcs-output) Add a **GCS output** to the bucket that receives your webhooks: - **Bucket name** (required). - **Prefix** (optional) — e.g. `stripe/`. - **Format** — `json` (full webhook with headers, the default), `body_only` (raw body), or `har`. Objects are written under a dated key so they're easy to browse and lifecycle: ``` {prefix}/{year}/{month}/{day}/{log_id}.json ``` ## [3. Point your provider at the bucket URL](#_3-point-your-provider-at-the-bucket-url) Use the bucket's public Webhook Relay endpoint as the webhook URL in your provider, then test: ``` curl -X POST https://your-webhook-relay-endpoint \ -H 'Content-Type: application/json' \ -d '{"event":"invoice.paid","id":"in_123"}' ``` An object appears in GCS within seconds. ## [Why archive webhooks](#why-archive-webhooks) - **Audit & compliance.** Keep an immutable record of what each provider sent, with retention via object lifecycle rules. - **Debug after the fact.** Inspect the exact payload that caused a problem, even days later. - **Replay.** Re-feed stored payloads into a new service or a rebuilt handler. - **Load into BigQuery.** Point an external table or a load job at the archive for analytics. ## [Going further](#going-further) - Forward to your app **and** archive at once with [multiple destinations](https://webhookrelay.com/blog/webhook-to-gcs/features/webhook-multiple-destinations/). - [Transform](https://webhookrelay.com/blog/webhook-to-gcs/features/transform-webhooks/) or [filter](https://webhookrelay.com/blog/webhook-to-gcs/features/forwarding-rules/) before storing. - Stream straight into a warehouse with [webhooks to BigQuery](https://webhookrelay.com/blog/webhook-to-gcs/blog/webhook-to-bigquery/), or use [S3](https://webhookrelay.com/blog/webhook-to-gcs/blog/webhook-to-s3/) on AWS. [Inspect a webhook first](https://webhookrelay.com/blog/webhook-to-gcs/webhook-bin/) or [create a free account](https://my.webhookrelay.com/register) to set up the GCS archive. --- --- title: Receive Emails as Webhooks: Inbound Parsing | WebhookRelay meta: "og: title": "Receive Emails as Webhooks: Inbound Parsing" description: Webhook Relay can receive email and turn it into a webhook — each message parsed to JSON (from, subject, text, HTML, attachments) and sent to your endpoint. url: https://webhookrelay.com/blog/receive-emails-as-webhooks.md file: /blog/receive-emails-as-webhooks.md --- ![Stripes](https://webhookrelay.com/blog/receive-emails-as-webhooks/images/stripes.svg) # **Receive Emails as Webhooks: Inbound Email Parsing & Forwarding** Webhook Relay can receive email and turn it into a webhook — each message parsed to JSON (from, subject, text, HTML, attachments) and sent to your endpoint. ![Receive emails as webhooks](https://webhookrelay.com/blog/receive-emails-as-webhooks/images/blog/heroes/route.jpg) A lot of useful information still arrives as **email** — support requests, server alerts, order confirmations, vendor notifications, and form submissions from tools that only email you. Acting on it usually means running a mail server, polling an IMAP inbox, or wiring up Zapier. Now there's a simpler option: **Webhook Relay can receive email for you and turn each message into a webhook.** ## [Receiving](#receiving) Add an **email input** to a bucket and Webhook Relay gives you a unique address like `5519fb0d-997a-46cd-b102-ac990b4c38fa@in.webhookrelay-mail.com`. Point any sender at it — there's no DNS or MX setup, and nothing to host. See [Email to Webhook](https://webhookrelay.com/blog/receive-emails-as-webhooks/email-to-webhook/) for the overview, or the [docs](https://webhookrelay.com/blog/receive-emails-as-webhooks/docs/email/) to set one up. ## [Parsing](#parsing) Every message is parsed server-side into clean JSON and delivered with `Content-Type: application/json`: ``` { "from": "jon@example.com", "from_name": "Jon Snow", "subject": "Can't log in to my account", "text": "Hi, I can't log in since this morning…", "html": "

Hi, I can't log in since this morning…

", "to": ["5519fb0d-…@in.webhookrelay-mail.com"], "spf": "pass", "dkim": "pass", "dmarc": "pass", "attachments": [ { "name": "screenshot.png", "content_type": "image/png", "size": 48213, "content": "iVBORw0KGgo…" } ] } ``` You get the sender, subject, both body parts, all headers, the SPF/DKIM/DMARC results, and attachments (base64-encoded). The full [payload reference](https://webhookrelay.com/blog/receive-emails-as-webhooks/docs/email/payload/) lists every field, and you can [restrict senders and cap attachments](https://webhookrelay.com/blog/receive-emails-as-webhooks/docs/email/filtering-and-policy/) per input. ## [Forwarding](#forwarding) Because the parsed email becomes an ordinary event on the bucket, you can do anything you'd do with a webhook: forward it to your endpoint (public or [behind a firewall](https://webhookrelay.com/blog/receive-emails-as-webhooks/webhooks/)), fan it out to several destinations, retry on failure, and [transform it in flight](https://webhookrelay.com/blog/receive-emails-as-webhooks/docs/webhooks/functions/) into whatever shape the destination expects. Build that message visually with the [Slack/Discord/Teams formatter](https://webhookrelay.com/blog/receive-emails-as-webhooks/webhook-message-formatter/). ## [Examples](#examples) Step-by-step tutorials for the common destinations: - [Email → Slack](https://webhookrelay.com/blog/receive-emails-as-webhooks/docs/tutorials/email/slack/) — post inbound email into a Slack channel - [Email → Discord](https://webhookrelay.com/blog/receive-emails-as-webhooks/docs/tutorials/email/discord/) — route email to a Discord channel - [Email → Microsoft Teams](https://webhookrelay.com/blog/receive-emails-as-webhooks/docs/tutorials/email/microsoft-teams/) — post a Teams card per email - [Email → Google Sheets](https://webhookrelay.com/blog/receive-emails-as-webhooks/docs/tutorials/email/google-sheets/) — append a row per email - [Email → Airtable](https://webhookrelay.com/blog/receive-emails-as-webhooks/docs/tutorials/email/airtable/) — create an Airtable record - [Email → Notion](https://webhookrelay.com/blog/receive-emails-as-webhooks/docs/tutorials/email/notion/) — create a page in a Notion database - [Email → your API](https://webhookrelay.com/blog/receive-emails-as-webhooks/docs/tutorials/email/api/) — POST email to your own endpoint - [Email → a database](https://webhookrelay.com/blog/receive-emails-as-webhooks/docs/tutorials/email/database/) — insert every email into your DB ## [Get started](#get-started) [Create an email input](https://my.webhookrelay.com) on a bucket, or read [the email docs](https://webhookrelay.com/blog/receive-emails-as-webhooks/docs/email/). It works on the free tier — same as any other Webhook Relay input. --- --- title: Webhook Relay Blog | WebhookRelay meta: "og: title": "Webhook Relay Blog" description: Guides, tutorials and comparisons for receiving, testing, securing and routing webhooks. url: https://webhookrelay.com/blog.md file: /blog.md --- # **The Webhook Relay Blog ** Guides, tutorials and comparisons for receiving, testing, securing, transforming and routing webhooks — from localhost to production. [**Guides**

**How to Send a Webhook from Google Forms (Apps Script)**

Google Forms has no built-in webhooks, but a few lines of Apps Script and an on-form-submit trigger will POST every response to your URL. Copy-paste code.Jul 21, 2026•3 min read](https://webhookrelay.com/blog/blog/google-forms-webhook/) [**Guides**

**How to Create a Microsoft Teams Webhook (Workflows, 2026)**

Microsoft retired Office 365 Connectors. Here's the current way to create a Teams incoming webhook with Workflows (Power Automate), step by step.Jul 21, 2026•3 min read](https://webhookrelay.com/blog/blog/microsoft-teams-webhook/) [**Guides**

**How to Get a Discord Webhook URL**

Create a Discord webhook URL step by step: Server Settings, Integrations, pick a channel, copy the URL. Includes the format, a curl example and embeds.Jul 21, 2026•2 min read](https://webhookrelay.com/blog/blog/how-to-get-discord-webhook-url/) [**Guides**

**How to Get a Slack Webhook URL (Incoming Webhooks)**

Get a Slack incoming webhook URL step by step: create a Slack app, enable Incoming Webhooks, pick a channel, and post with curl. Includes the format.Jul 21, 2026•3 min read](https://webhookrelay.com/blog/blog/how-to-get-slack-webhook-url/) [**Guides**

**Twilio Webhooks: Setup, Payloads, Signature Validation & Testing**

A complete guide to Twilio webhooks: configuring SMS and voice, the form-encoded payload, validating X-Twilio-Signature, retries, and local testing.Jul 19, 2026•4 min read](https://webhookrelay.com/blog/blog/twilio-webhooks-guide/) [**Guides**

**Webhook Architecture Diagram: How Webhooks Flow End to End**

A clear webhook architecture diagram — from the sender that emits an event to the endpoint that receives it, plus queues, retries and signature checks.Jul 19, 2026•3 min read](https://webhookrelay.com/blog/blog/webhook-architecture-diagram/) [**Guides**

**Webhook vs WebSocket: What's the Difference?**

Webhooks and WebSockets both deliver real-time data, but one is a short one-way callback and the other a persistent connection. When to use each.Jul 19, 2026•3 min read](https://webhookrelay.com/blog/blog/webhook-vs-websocket/) [**Guides**

**Receiving Webhooks in CI/CD: GitHub Actions, Jenkins & Harness (2026)**

CI/CD runners are ephemeral and private, so providers can't reach them. How to receive webhooks in GitHub Actions, Jenkins and Harness behind a firewall.Jul 18, 2026•4 min read](https://webhookrelay.com/blog/blog/webhooks-in-ci-cd/) [**Comparisons**

**An ngrok Alternative for Webhooks: Persistent URLs & Team Broadcast (2026)**

An ngrok alternative for webhooks: a persistent URL that never changes, shared across your team so everyone receives the same Stripe or GitHub events.Jul 18, 2026•6 min read](https://webhookrelay.com/blog/blog/ngrok-alternative/) [**Tutorials**

**Splunk Webhook Alerts: Payload, Limits and Fan-Out**

Wire Splunk alert actions to webhooks: the exact JSON payload, the action's limits (no custom headers), URL allowlisting, and fan-out to Slack.Jul 7, 2026•3 min read](https://webhookrelay.com/blog/blog/splunk-webhook-alerts/) [**Tutorials**

**Confluence Webhooks: Your Options on Cloud and Data Center**

How to get webhooks out of Confluence — Data Center's native webhooks, Cloud's Automation web requests and Forge apps — plus payloads and testing.Jul 7, 2026•3 min read](https://webhookrelay.com/blog/blog/confluence-webhooks/) [**Tutorials**

**Zendesk Webhooks: Setup, Signatures and Payload Examples**

Create Zendesk webhooks the right way: triggers vs event subscriptions, the payload format, verifying X-Zendesk-Webhook-Signature, and retries.Jul 7, 2026•3 min read](https://webhookrelay.com/blog/blog/zendesk-webhooks-guide/) [**Tutorials**

**DocuSign Connect Webhooks: Events, HMAC and Retry Behavior**

A practical guide to DocuSign Connect webhooks: envelope and recipient events, the JSON payload, HMAC verification, and Connect's retry behavior.Jul 7, 2026•4 min read](https://webhookrelay.com/blog/blog/docusign-connect-webhooks/) [View all posts](https://webhookrelay.com/blog/blog-all/) --- --- title: Email to Notion | WebhookRelay meta: "og: title": "Email to Notion" description: Capture any inbound email into a Notion database — Webhook Relay parses the email to JSON and a transform turns it into a new Notion page, automatically. url: https://webhookrelay.com/docs/tutorials/email/notion.md file: /docs/tutorials/email/notion.md --- ![Stripes](https://webhookrelay.com/docs/tutorials/email/notion/images/stripes.svg) Documentation **Fundamentals** # **Email to Notion** Capture any inbound email into a Notion database — Webhook Relay parses the email to JSON and a transform turns it into a new Notion page, automatically. Plenty of things worth tracking still arrive as **email**: inbound leads, support requests, newsletters and articles you mean to read later, vendor notifications. Notion is where many teams keep that kind of structured record — but copy-pasting each message into a database by hand doesn't scale. You want every email to become a **Notion page** automatically, with the subject, sender and body already filled in. [Webhook Relay](https://webhookrelay.com) can [receive emails as webhooks](https://webhookrelay.com/docs/tutorials/email/notion/docs/email/). You give an inbox a unique address, every message sent to it is parsed into JSON, and a small **transform** reshapes it into the format the Notion API expects before delivery — no mail server, no glue code, no polling. ## [How it works](#how-it-works) The whole path is a single hop with a transform in the middle: ``` inbound email ──▶ Webhook Relay (parse + transform) ──▶ Notion API ──▶ new page ``` When an email hits your Webhook Relay address, it's parsed into a structured JSON document and POSTed to the bucket's outputs. Notion's [create-a-page endpoint](https://developers.notion.com/reference/post-page) doesn't understand that shape — it expects a `parent`, a `properties` object and optional `children` blocks — so the transform converts one into the other in flight. The parsed email looks like this: ``` { "from": "jon@example.com", "from_name": "Jon Snow", "recipient": "@in.webhookrelay-mail.com", "subject": "Partnership inquiry", "date": "Fri, 26 Jun 2026 11:27:41 +0400", "text": "plain body", "html": "

html body

", "spf": "pass", "dkim": "pass", "dmarc": "pass", "attachments": [] } ``` See the full [payload reference](https://webhookrelay.com/docs/tutorials/email/notion/docs/email/payload/) for every field, including `to`, `cc`, `message_id` and the raw `headers` map. ## [Setup](#setup) **1. Create a bucket.** This is the container that ties an input to its outputs. **2. Add an email input.** Webhook Relay gives you back a unique address of the form `@in.webhookrelay-mail.com`. Anything sent there is parsed and forwarded to the bucket's outputs. **3. Create a Notion integration and share the database.** In Notion, create an internal integration to get a secret token, then open your target database, click _Connections_, and share it with that integration. Copy the database ID from its URL. **4. Add the Notion API as an output.** Set the bucket's output destination to `https://api.notion.com/v1/pages`. **5. Attach the transform.** Add the [transform function](https://webhookrelay.com/docs/tutorials/email/notion/docs/webhooks/functions/) below so the parsed email is reshaped into Notion's format — and the required headers are set — before it's delivered. **6. Send a test email.** Email your `@in.webhookrelay-mail.com` address from any client. Within a second or two a new page appears in your database. If it doesn't, open the request log in your dashboard to see exactly what was received and what Notion returned. ## [The transform](#the-transform) Webhook Relay [functions](https://webhookrelay.com/docs/tutorials/email/notion/docs/webhooks/functions/) hand you the incoming request body as a string in `r.body`. Parse it, build the Notion page payload, and write it back with `r.setBody` — then set the three headers Notion requires: ``` const e = JSON.parse(r.body); r.setBody(JSON.stringify({ parent: { database_id: "YOUR_DB_ID" }, properties: { Name: { title: [{ text: { content: e.subject } }] }, From: { email: e.from } }, children: [{ object: "block", type: "paragraph", paragraph: { rich_text: [{ text: { content: e.text || "" } }] } }] })); r.setHeader("Authorization", "Bearer " + "YOUR_NOTION_SECRET"); r.setHeader("Notion-Version", "2022-06-28"); r.setHeader("Content-Type", "application/json"); ``` That's the complete bridge: the subject fills the `Name` title property, the sender lands in a `From` email property, and the plain-text body becomes a paragraph block in the page. Notion creates the page and returns it. From here the function is where the real power is. Map more email fields to columns — `e.date` to a date property, `e.from_name` to a text property — split the body into multiple blocks, or inspect `e.spf` / `e.from` and `return` early to drop anything you don't want stored. ## [Get started](#get-started) Turning inbound mail into Notion pages is one case of a broader pattern: [Email to Webhook](https://webhookrelay.com/docs/tutorials/email/notion/email-to-webhook/). Once an email is JSON, you can route it anywhere a webhook can go. [Create a free Webhook Relay account](https://my.webhookrelay.com/register), add an email input, and have leads, support requests and reading-list items flowing into Notion in a few minutes. Did this page help you? --- --- title: Email to API | WebhookRelay meta: "og: title": "Email to API" description: Convert inbound email into an API request — Webhook Relay parses each message to JSON and a transform POSTs it to your API in the shape it expects. url: https://webhookrelay.com/docs/tutorials/email/api.md file: /docs/tutorials/email/api.md --- ![Stripes](https://webhookrelay.com/docs/tutorials/email/api/images/stripes.svg) Documentation **Fundamentals** # **Email to API** Convert inbound email into an API request — Webhook Relay parses each message to JSON and a transform POSTs it to your API in the shape it expects. You have an API — or an app behind one — and a steady stream of inbound email you'd like to ingest as structured requests: support tickets, order confirmations, server alerts, form notifications, customer replies. The usual options are unappealing: run and secure an SMTP server, or poll an IMAP mailbox on a timer and parse raw MIME yourself. Both are infrastructure you don't want to own. [Webhook Relay](https://webhookrelay.com) can [receive emails as webhooks](https://webhookrelay.com/docs/tutorials/email/api/docs/email/) instead. You get a unique address, every message sent to it is parsed into JSON, and a small **transform** reshapes that JSON into exactly the request your API expects — body, headers and auth — before it's POSTed to your endpoint. ## [How it works](#how-it-works) The whole path is a single hop with a transform in the middle: ``` inbound email ──▶ Webhook Relay (parse + transform) ──▶ POST to your API (public or behind a firewall) ``` When an email hits your Webhook Relay address it's parsed into a structured JSON document and POSTed to the bucket's outputs. Your API expects its own request shape — not Webhook Relay's parsed-email shape — so the transform converts one into the other in flight, and can attach the headers and bearer token your endpoint requires. The parsed email looks like this: ``` { "from": "jon@example.com", "from_name": "Jon Snow", "recipient": "@in.webhookrelay-mail.com", "to": ["@in.webhookrelay-mail.com"], "cc": [], "subject": "Order #4821 confirmed", "date": "Fri, 26 Jun 2026 11:27:41 +0400", "message_id": "", "text": "plain body", "html": "

html body

", "headers": {}, "spf": "pass", "dkim": "pass", "dmarc": "pass", "attachments": [ { "name": "invoice.pdf", "content_type": "application/pdf", "size": 48213, "content": "", "truncated": false } ] } ``` See the full [payload reference](https://webhookrelay.com/docs/tutorials/email/api/docs/email/payload/) for every field. ## [Setup](#setup) **1. Create a bucket.** This is the container that ties an input to its outputs. **2. Add an email input.** Webhook Relay gives you back a unique address of the form `@in.webhookrelay-mail.com`. Anything sent there is parsed and forwarded to the bucket's outputs. **3. Add your API as the output.** Point the bucket's output at your endpoint's URL. Static headers and auth can be set on the output itself, or in the transform below — whichever you prefer. **4. Attach a transform.** Add the [transform function](https://webhookrelay.com/docs/tutorials/email/api/docs/webhooks/functions/) below so the parsed email is reshaped into your API's schema before delivery. **5. Send a test email.** Email your `@in.webhookrelay-mail.com` address from any client. Within a second or two the request reaches your API. If it doesn't, open the request log in your dashboard to see exactly what was received and delivered. ## [The transform](#the-transform) Webhook Relay [functions](https://webhookrelay.com/docs/tutorials/email/api/docs/webhooks/functions/) hand you the incoming request body as a string in `r.body`. Parse it, build your API's request, and write it back with `r.setBody()` and `r.setHeader()`: ``` const e = JSON.parse(r.body); r.setBody(JSON.stringify({ sender: e.from, title: e.subject, body: e.text, received_at: e.date })); r.setHeader("Authorization", "Bearer YOUR_API_TOKEN"); r.setHeader("Content-Type", "application/json"); ``` That's the complete bridge: the email is mapped field-for-field onto your API's schema and authenticated with your token. The same function is the place to **drop** messages you don't want. Inspect any parsed field and short-circuit with `r.setResponseStatus()` plus an early `return` so the request never reaches your endpoint: ``` const e = JSON.parse(r.body); if (e.dmarc !== "pass") { r.setResponseStatus(202); return; // discard spoofed mail — never hits your API } ``` Only the messages you actually want make the trip. For coarser control, pair this with the From-address allowlist in [filtering and policy](https://webhookrelay.com/docs/tutorials/email/api/docs/email/filtering-and-policy/). Outputs aren't limited to public URLs. With the relay agent, Webhook Relay can [deliver to a server behind a firewall](https://webhookrelay.com/docs/tutorials/email/api/webhooks/) or on localhost with no public IP — useful for internal services and local development. Failed deliveries are retried, so a brief outage on your side doesn't drop email. ## [Get started](#get-started) Turning inbound mail into API requests is one case of a broader pattern: [Email to Webhook](https://webhookrelay.com/docs/tutorials/email/api/email-to-webhook/). Once an email is JSON, you can route it anywhere a webhook can go. [Create a free Webhook Relay account](https://my.webhookrelay.com/register), add an email input, and start ingesting inbound email as clean API requests in a few minutes. Did this page help you? --- --- title: Email to Airtable | WebhookRelay meta: "og: title": "Email to Airtable" description: Turn inbound emails into Airtable records automatically. Webhook Relay parses email to JSON, then a transform maps it into the Airtable API format. url: https://webhookrelay.com/docs/tutorials/email/airtable.md file: /docs/tutorials/email/airtable.md --- ![Stripes](https://webhookrelay.com/docs/tutorials/email/airtable/images/stripes.svg) Documentation **Fundamentals** # **Email to Airtable** Turn inbound emails into Airtable records automatically. Webhook Relay parses email to JSON, then a transform maps it into the Airtable API format. A lot of structured information still arrives as **email**: inbound leads, order confirmations, job applications, vendor notifications, form submissions that only email you. Each one is a row you wish were in **Airtable** — but instead it sits in an inbox waiting to be copied across by hand. [Webhook Relay](https://webhookrelay.com) can [receive emails as webhooks](https://webhookrelay.com/docs/tutorials/email/airtable/docs/email/). You give an inbox a unique address, every message sent to it is parsed into JSON, and a small **transform** reshapes it into the format the [Airtable Web API](https://airtable.com/developers/web/api/introduction) expects before delivery — no mail server, no parsing of raw MIME, no polling. ## [How it works](#how-it-works) The whole path is a single hop with a transform in the middle: ``` inbound email ──▶ Webhook Relay (parse + transform) ──▶ Airtable API ──▶ new record ``` When an email hits your Webhook Relay address, it's parsed into a structured JSON document and POSTed to the bucket's outputs. The Airtable API doesn't understand that shape — it accepts a `{ "fields": { ... } }` body with a bearer token — so the transform converts one into the other in flight. The parsed email looks like this: ``` { "from": "jon@example.com", "from_name": "Jon Snow", "recipient": "@in.webhookrelay-mail.com", "to": ["@in.webhookrelay-mail.com"], "subject": "New lead: Acme Corp", "date": "Sun, 28 Jun 2026 11:27:41 +0400", "message_id": "", "text": "plain body", "html": "

html body

", "spf": "pass", "dkim": "pass", "dmarc": "pass", "attachments": [ { "name": "resume.pdf", "content_type": "application/pdf", "size": 48213, "content": "" } ] } ``` See the full [payload reference](https://webhookrelay.com/docs/tutorials/email/airtable/docs/email/payload/) for every field, including `cc` and the raw `headers` map. ## [Setup](#setup) **1. Create a bucket.** This is the container that ties an input to its outputs. **2. Add an email input.** Webhook Relay gives you back a unique address of the form `@in.webhookrelay-mail.com`. Anything sent there is parsed and forwarded to the bucket's outputs. **3. Add an output pointing at the Airtable API.** Create an Airtable personal access token with the `data.records:write` scope, then add an output whose destination is `https://api.airtable.com/v0//`. Put the credentials in the output's headers: `Authorization: Bearer ` and `Content-Type: application/json`. **4. Attach the transform.** Add the [transform function](https://webhookrelay.com/docs/tutorials/email/airtable/docs/webhooks/functions/) below so the parsed email is reshaped into an Airtable record before it's delivered. **5. Send a test email.** Email your `@in.webhookrelay-mail.com` address from any client. Within a second or two a new row appears in your table. If it doesn't, open the request log in your dashboard to see exactly what was received and delivered. ## [The transform](#the-transform) Webhook Relay [functions](https://webhookrelay.com/docs/tutorials/email/airtable/docs/webhooks/functions/) hand you the incoming request body as a string in `r.body`. Parse it, build the Airtable payload, and write it back with `r.setBody`: ``` const e = JSON.parse(r.body); r.setBody(JSON.stringify({ fields: { From: e.from, Subject: e.subject, Body: e.text, Received: e.date } })); r.setHeader("Authorization", "Bearer " + "YOUR_AIRTABLE_TOKEN"); r.setHeader("Content-Type", "application/json"); ``` That's the complete bridge: the sender, subject, body and date become a single Airtable record. Each key under `fields` must match a column in your table and use a compatible value type (single line text, long text, date, single select). For cleaner data, prefer `e.from_name` over `e.from` for a display name, and trim or truncate `e.text` if your column has limits. To handle **attachments**, remember each entry in `e.attachments` carries base64 `content` — upload it to your own storage first, then put the resulting URL in an Airtable attachment field, since Airtable fetches attachments by URL. > In production, keep the token in the output's configured headers rather than hardcoding it in the function, so it never lives in your transform source. ## [Get started](#get-started) Turning inbound mail into Airtable records is one case of a broader pattern: [Email to Webhook](https://webhookrelay.com/docs/tutorials/email/airtable/email-to-webhook/). Once an email is JSON, you can route it anywhere a webhook can go. [Create a free Webhook Relay account](https://my.webhookrelay.com/register), add an email input, and have leads, orders and applications flowing into Airtable in a few minutes. Did this page help you? --- --- title: Email to Slack | WebhookRelay meta: "og: title": "Email to Slack" description: Send any inbound email to a Slack channel — Webhook Relay parses the email to JSON and a transform turns it into a Slack message, automatically. url: https://webhookrelay.com/docs/tutorials/email/slack.md file: /docs/tutorials/email/slack.md --- ![Stripes](https://webhookrelay.com/docs/tutorials/email/slack/images/stripes.svg) Documentation **Fundamentals** # **Email to Slack** Send any inbound email to a Slack channel — Webhook Relay parses the email to JSON and a transform turns it into a Slack message, automatically. Plenty of useful things still arrive as **email**: support requests, server alerts, order confirmations, form submissions that only email you, vendor notifications. The trouble is that email is where they go to be missed. You want each of those messages to land in a **Slack channel** automatically, where your team actually looks. [Webhook Relay](https://webhookrelay.com) can [receive emails as webhooks](https://webhookrelay.com/docs/tutorials/email/slack/docs/email/). You give an inbox a unique address, every message sent to it is parsed into JSON, and a small **transform** reshapes it into the format Slack expects before delivery — no mail server, no glue code, no polling. > **Build the message visually:** the free [webhook → Slack/Discord/Teams formatter](https://webhookrelay.com/docs/tutorials/email/slack/webhook-message-formatter/) lets you template the Slack message from a sample payload and copy the transform. ## [How it works](#how-it-works) The whole path is a single hop with a transform in the middle: ``` inbound email ──▶ Webhook Relay (parse) ──▶ transform to Slack { text } ──▶ Slack channel ``` When an email hits your Webhook Relay address, it's parsed into a structured JSON document and POSTed to the bucket's outputs. A Slack [incoming webhook](https://api.slack.com/messaging/webhooks) doesn't understand that shape — it only accepts `{ "text": "..." }` or a Block Kit `blocks` array — so the transform converts one into the other in flight. The parsed email looks like this: ``` { "from": "jon@example.com", "from_name": "Jon Snow", "recipient": "@in.webhookrelay-mail.com", "subject": "Order #4821 confirmed", "date": "Fri, 26 Jun 2026 11:27:41 +0400", "text": "plain body", "html": "

html body

", "spf": "pass", "dkim": "pass", "dmarc": "pass", "attachments": [ { "name": "invoice.pdf", "content_type": "application/pdf", "size": 48213, "content": "", "truncated": false } ] } ``` See the full [payload reference](https://webhookrelay.com/docs/tutorials/email/slack/docs/email/payload/) for every field, including `to`, `cc`, `message_id` and the raw `headers` map. ## [Setup](#setup) **1. Create a bucket.** This is the container that ties an input to its outputs. **2. Add an email input.** Webhook Relay gives you back a unique address of the form `@in.webhookrelay-mail.com`. Anything sent there is parsed and forwarded to the bucket's outputs. **3. Add a Slack output.** Create a Slack incoming webhook URL (`https://hooks.slack.com/services/T0000/B0000/XXXXXXXX`) for the channel you want to post to, and add it as the bucket's output destination. Each Slack incoming webhook is bound to one channel. **4. Attach the transform.** Add the [transform function](https://webhookrelay.com/docs/tutorials/email/slack/docs/webhooks/functions/) below so the parsed email is reshaped into Slack's format before it's delivered. **5. Send a test email.** Email your `@in.webhookrelay-mail.com` address from any client. Within a second or two the message appears in your Slack channel. If it doesn't, open the request log in your dashboard to see exactly what was received and delivered. ## [The transform](#the-transform) Webhook Relay [functions](https://webhookrelay.com/docs/tutorials/email/slack/docs/webhooks/functions/) hand you the incoming request body as a string in `r.body`. Parse it, build the Slack payload, and write it back with `r.setBody`: ``` const email = JSON.parse(r.body); const slack = { text: \`:email: *${email.subject}*\nFrom: ${email.from_name || email.from}\n\n${email.text}\`, }; r.setBody(JSON.stringify(slack)); r.setHeader("Content-Type", "application/json"); ``` That's the complete bridge: the email's subject becomes the headline, the sender is attributed, and the plain-text body follows. Slack accepts the `{ text }` body and posts it. From here the function is where the real power is. Swap the flat string for a Slack **Block Kit** `blocks` array to render the subject, sender and body as a formatted message with fields, dividers and buttons. You can also fan out to **multiple channels** by adding more outputs — one Slack incoming webhook per channel, or Slack _and_ Discord at once — and shape the payload differently for each. To keep noise down, restrict who can email the address with the [From-address allowlist](https://webhookrelay.com/docs/tutorials/email/slack/docs/email/filtering-and-policy/), or inspect `email.from` / `email.spf` inside the function and `return` early to drop anything you don't want forwarded. ## [Get started](#get-started) Turning inbound mail into Slack messages is one case of a broader pattern: [Email to Webhook](https://webhookrelay.com/docs/tutorials/email/slack/email-to-webhook/). Once an email is JSON, you can route it anywhere a webhook can go. [Create a free Webhook Relay account](https://my.webhookrelay.com/register), add an email input, and have support requests, alerts and confirmations flowing into Slack in a few minutes. Did this page help you? --- --- title: Email to Discord | WebhookRelay meta: "og: title": "Email to Discord" description: Forward inbound email to a Discord channel automatically. Webhook Relay parses each email and a transform reshapes it into Discord's { content } format. url: https://webhookrelay.com/docs/tutorials/email/discord.md file: /docs/tutorials/email/discord.md --- ![Stripes](https://webhookrelay.com/docs/tutorials/email/discord/images/stripes.svg) Documentation **Fundamentals** # **Email to Discord** Forward inbound email to a Discord channel automatically. Webhook Relay parses each email and a transform reshapes it into Discord's { content } format. You get emails you'd rather see in chat — monitoring alerts, contact-form submissions, order confirmations, vendor notifications — and you want them dropped into a **Discord channel** automatically instead of buried in an inbox nobody's watching. The problem: there's no native "email this to Discord" button, and Discord only accepts a specific JSON shape, not a raw email. [Webhook Relay](https://webhookrelay.com) closes that gap. It gives you an address that **receives inbound email and delivers it as a webhook**, parsing each message into clean JSON. A small transform then reshapes that JSON into the format Discord expects, and the message appears in your channel — no server, no inbox polling, no glue code. > **Build the message visually:** the free [webhook → Slack/Discord/Teams formatter](https://webhookrelay.com/docs/tutorials/email/discord/webhook-message-formatter/) lets you template the Discord embed and copy the transform. ## [How it works](#how-it-works) Email comes in, gets parsed, gets reshaped, and lands in Discord — a single hop with a transform in the middle: ``` inbound email ──▶ Webhook Relay (parse) ──▶ transform to Discord ──▶ channel (any sender) (email → JSON) (serverless function) (message appears) ``` Webhook Relay does the hard part — accepting SMTP, parsing MIME, handling attachments and SPF/DKIM/DMARC — and hands your function a tidy JSON object. The [transform function](https://webhookrelay.com/docs/tutorials/email/discord/docs/webhooks/functions/) only has to map a couple of fields. This is the same email side that powers [Email to Webhook](https://webhookrelay.com/docs/tutorials/email/discord/email-to-webhook/); here the destination just happens to be Discord. ## [Setup](#setup) **1. Create a bucket.** In the dashboard, create a bucket — it's the container that ties an input (where email arrives) to outputs (where it's delivered). **2. Add an email input.** On the bucket, add an _email input_. Webhook Relay gives you a unique address of the form `@in.webhookrelay-mail.com`. Anything sent to that address is parsed and POSTed as JSON to the bucket's outputs — see [receive emails as webhooks](https://webhookrelay.com/docs/tutorials/email/discord/docs/email/) for the full setup. **3. Add a Discord output.** In Discord, open **Server Settings → Integrations → Webhooks → New Webhook**, pick the channel, and **Copy Webhook URL**. Paste that URL as the bucket's output destination. **4. Attach a transform.** Add the [transform function](https://webhookrelay.com/docs/tutorials/email/discord/docs/webhooks/functions/) below so the parsed email is reshaped into Discord's `{ content }` format before delivery. **5. Test.** Send an email to your `@in.webhookrelay-mail.com` address. Within a second or two it shows up in your Discord channel. If it doesn't, open the bucket's request log to see exactly what was received and delivered. ## [The transform](#the-transform) Each email arrives as JSON with fields like `from`, `from_name`, `recipient`, `to`, `cc`, `subject`, `date`, `message_id`, `text`, `html`, `headers`, `spf`, `dkim`, `dmarc`, and `attachments`. See the full [payload reference](https://webhookrelay.com/docs/tutorials/email/discord/docs/email/payload/) for every field. In the transform, the body is `r.body` (a string); reshape it and set the new body: ``` const email = JSON.parse(r.body); const discord = { content: \`📧 **${email.subject}**\nFrom: ${email.from_name || email.from}\n\n${email.text}\` }; r.setBody(JSON.stringify(discord)); r.setHeader("Content-Type", "application/json"); ``` That parses the incoming email, builds Discord's expected `{ "content": "..." }` body from the subject, sender and plain-text body, and sets the right header. You can also `r.setResponseStatus(code)` if you need to control what's returned. A Discord incoming webhook accepts either `{ "content": "..." }` or an `embeds` array — so for a richer card, build an `embeds` object instead (map `email.subject` to the title and `email.text` to the description) and Discord will render it with a title, colored sidebar, and fields. ## [Going further](#going-further) Want only certain senders to reach Discord? Add a From allowlist with [filtering and policy](https://webhookrelay.com/docs/tutorials/email/discord/docs/email/filtering-and-policy/) so unwanted mail never gets delivered. And because this is the same inbound-email pipeline behind [Email to Webhook](https://webhookrelay.com/docs/tutorials/email/discord/email-to-webhook/), you can point the parsed JSON anywhere — Discord today, Slack, an internal API, or several destinations at once tomorrow. [Create a free Webhook Relay account](https://my.webhookrelay.com/register), add an email input, and route your first message into Discord in a few minutes — see the [Email to Webhook](https://webhookrelay.com/docs/tutorials/email/discord/email-to-webhook/) pillar for the full picture. Building a Discord [incoming webhook](https://discord.com/developers/docs/resources/webhook)? The format above is everything it needs. Did this page help you? --- --- title: Email to Google Sheets | WebhookRelay meta: "og: title": "Email to Google Sheets" description: Parse email to a Google Sheet automatically. Give an inbox a unique address, parse each message to JSON, transform the fields, and append a spreadsheet row. url: https://webhookrelay.com/docs/tutorials/email/google-sheets.md file: /docs/tutorials/email/google-sheets.md --- ![Stripes](https://webhookrelay.com/docs/tutorials/email/google-sheets/images/stripes.svg) Documentation **Fundamentals** # **Email to Google Sheets** Parse email to a Google Sheet automatically. Give an inbox a unique address, parse each message to JSON, transform the fields, and append a spreadsheet row. You receive structured emails all day — order confirmations, inbound leads, contact-form notifications, monitoring digests, scheduled reports — and you want each one captured as a row in **Google Sheets**. Copy-pasting is tedious, and reaching for Zapier or a hosted email-parsing SaaS is overkill for something this mechanical. [Webhook Relay](https://webhookrelay.com) does it directly. Add an [email input](https://webhookrelay.com/docs/tutorials/email/google-sheets/email-to-webhook/) to a bucket and you get a unique address. Every message sent there is **parsed to JSON**, optionally reshaped by a transform, and delivered to a destination that appends a spreadsheet row. No polling, no glue server, no IMAP. ## [How it works](#how-it-works) The whole pipeline is a single hop: ``` inbound email ──▶ Webhook Relay ──▶ Apps Script Web App ──▶ appendRow ──▶ Google Sheet (any sender) (parse + transform) (doPost endpoint) (one row) (in your sheet) ``` Webhook Relay [receives emails as webhooks](https://webhookrelay.com/docs/tutorials/email/google-sheets/docs/email/): you get a unique address like `@in.webhookrelay-mail.com`, each email is parsed into a structured JSON document, and that document is POSTed to your bucket outputs. A small [transform function](https://webhookrelay.com/docs/tutorials/email/google-sheets/docs/webhooks/functions/) picks the columns you care about, and a Google Apps Script Web App writes the row. The parsed payload includes everything you'd want as a column: ``` { "from": "alice@acme.com", "from_name": "Alice", "recipient": "8c2f...@in.webhookrelay-mail.com", "to": ["orders@yourco.com"], "cc": [], "subject": "Order #4821 confirmed", "date": "2026-06-28T11:59:00Z", "message_id": "<...>", "text": "Plain-text body...", "html": "

HTML body...

", "headers": {}, "spf": "pass", "dkim": "pass", "dmarc": "pass", "attachments": [] } ``` See the full [payload reference](https://webhookrelay.com/docs/tutorials/email/google-sheets/docs/email/payload/) for every field. ## [Step 1: Create a bucket and add an email input](#step-1-create-a-bucket-and-add-an-email-input) In the dashboard, create a bucket, then add an **email input** to it. Webhook Relay generates a unique address: ``` 8c2f1e3a-...-d9b4@in.webhookrelay-mail.com ``` Anything sent to that address is parsed and forwarded to the bucket's outputs. Set up forwarding to that address from your provider, or use it directly as the destination on your forms and order system. ## [Step 2: Deploy a Google Apps Script Web App](#step-2-deploy-a-google-apps-script-web-app) Create a Google Sheet, open **Extensions → Apps Script**, and replace the default code with a `doPost(e)` handler that appends a row: ``` function doPost(e) { const sheet = SpreadsheetApp.openById("YOUR_SHEET_ID").getSheets()[0]; const email = JSON.parse(e.postData.contents); sheet.appendRow([ email.date || new Date(), email.from, email.subject, email.text, ]); return ContentService .createTextOutput(JSON.stringify({ status: "ok" })) .setMimeType(ContentService.MimeType.JSON); } ``` Click **Deploy → New deployment**, choose **Web app**, set **Execute as: Me** and **Who has access: Anyone**, then deploy and copy the **Web app URL**. It looks like `https://script.google.com/macros/s/AKfyc.../exec`. Re-publish a new version whenever you change the script. ## [Step 3: Add the Web App URL as a bucket output](#step-3-add-the-web-app-url-as-a-bucket-output) Back in Webhook Relay, add an **output** to your bucket and paste the Apps Script **Web app URL** as the destination. Now every parsed email is POSTed there and appended as a row. ## [Step 4 (optional): Transform to pick columns](#step-4-optional-transform-to-pick-columns) The parsed email JSON carries far more than you probably want in a spreadsheet. Attach a [transform function](https://webhookrelay.com/docs/tutorials/email/google-sheets/docs/webhooks/functions/) to the output to reduce it to exactly the columns you'll write: ``` const e = JSON.parse(r.body); r.setBody(JSON.stringify({ date: e.date, from: e.from, subject: e.subject, body: e.text })); ``` This keeps the four fields the Apps Script reads and drops the rest — headers, HTML, auth results — so the row stays clean. Adjust the keys here and the `appendRow([...])` call together to capture whatever you need. ## [Step 5: Send a test email](#step-5-send-a-test-email) Send a message to your `@in.webhookrelay-mail.com` address. Within a second or two a new row — date, from, subject, body — appears in your sheet. If nothing shows up, open the bucket's request log to see exactly what was received, parsed, and delivered. ## [Going further](#going-further) **Fan out to other automation.** If you'd rather not use Apps Script, point the output at any other endpoint that appends rows — a serverless function, an n8n/Make webhook, or your own API. The parsed JSON is the same; only the destination changes. **Forward only some senders.** You don't have to log everything. Use [input filtering and policy](https://webhookrelay.com/docs/tutorials/email/google-sheets/docs/email/filtering-and-policy/) to accept mail only from trusted senders or domains and drop the rest before parsing. **Capture more columns.** Add fields in both the transform body and the `appendRow([...])` call — recipient, message IDs, SPF/DKIM/DMARC results — to build a richer log straight from your inbox. This is one of Webhook Relay's [Email to Webhook](https://webhookrelay.com/docs/tutorials/email/google-sheets/email-to-webhook/) recipes: give any inbox a programmable address, parse each message to JSON, and route it anywhere. Google Sheets is just one destination — the same pattern delivers parsed email to a database, a chat channel, or an internal API. ## [Get started](#get-started) [Create a free Webhook Relay account](https://my.webhookrelay.com/register), add an email input to a bucket, and turn your inbox into a spreadsheet in a few minutes. Did this page help you? --- --- title: Email to Database | WebhookRelay meta: "og: title": "Email to Database" description: Store every inbound email in your own database. Webhook Relay parses email to JSON and POSTs it to a tiny insert endpoint you run — even one behind a firewall. url: https://webhookrelay.com/docs/tutorials/email/database.md file: /docs/tutorials/email/database.md --- ![Stripes](https://webhookrelay.com/docs/tutorials/email/database/images/stripes.svg) Documentation **Fundamentals** # **Email to Database** Store every inbound email in your own database. Webhook Relay parses email to JSON and POSTs it to a tiny insert endpoint you run — even one behind a firewall. You want every inbound email stored in **your own database** — an archive, an audit trail, or a queue your app reads from. Doing that the traditional way means running a mail server or polling an IMAP mailbox, parsing raw MIME, and writing a daemon to keep it alive. [Webhook Relay](https://webhookrelay.com) removes all of that. It [receives emails as webhooks](https://webhookrelay.com/docs/tutorials/email/database/docs/email/): you get a unique address, every message sent to it is parsed into JSON, and that JSON is POSTed to a tiny HTTP endpoint you run — which does a single `INSERT`. No mail server, no IMAP loop. ## [How it works](#how-it-works) Webhook Relay doesn't connect to your database directly. It parses the email and POSTs it to an HTTP handler you control, and that handler owns the SQL: ``` inbound email ──▶ Webhook Relay (parse) ──▶ POST ──▶ your insert endpoint ──▶ database ``` The parsed email looks like this: ``` { "from": "jon@example.com", "from_name": "Jon Snow", "recipient": "@in.webhookrelay-mail.com", "to": ["@in.webhookrelay-mail.com"], "subject": "Invoice #4821", "date": "Sun, 28 Jun 2026 11:27:41 +0400", "message_id": "", "text": "plain body", "html": "

html body

", "spf": "pass", "dkim": "pass", "dmarc": "pass", "attachments": [ { "name": "invoice.pdf", "content_type": "application/pdf", "size": 48213, "content": "", "truncated": false } ] } ``` See the full [payload reference](https://webhookrelay.com/docs/tutorials/email/database/docs/email/payload/) for every field, including `cc` and the raw `headers` map. ## [Setup](#setup) **1. Create a bucket.** This ties an input to its outputs. **2. Add an email input.** Webhook Relay returns a unique address of the form `@in.webhookrelay-mail.com`. Anything sent there is parsed and forwarded to the bucket's outputs. **3. Run your insert endpoint.** A small HTTP service that accepts a POST and writes one row (see below). **4. Add its URL as the output.** Point the bucket's output at your endpoint, e.g. `https://api.example.com/inbound`. **5. (Optional) attach a transform** to trim the payload to just the columns you store. **6. Send a test email.** Email your `@in.webhookrelay-mail.com` address. Within a second or two a row appears. If it doesn't, open the request log in your dashboard to see exactly what was received and delivered. ## [The handler](#the-handler) The endpoint receives the email JSON and inserts it. Use `message_id` with a `UNIQUE` constraint so the write is idempotent: ``` app.post('/inbound', async (req, res) => { const e = req.body; // the parsed email JSON await db.query( 'INSERT INTO emails(message_id, sender, subject, body, received_at) VALUES($1,$2,$3,$4,$5) ON CONFLICT (message_id) DO NOTHING', [e.message_id, e.from, e.subject, e.text, e.date] ); res.sendStatus(200); }); ``` That `ON CONFLICT (message_id) DO NOTHING` is what makes retries safe. Webhook Relay [durable retries](https://webhookrelay.com/docs/tutorials/email/database/docs/webhooks/durable-webhooks/) re-attempt any delivery your endpoint didn't acknowledge with a `2xx`, so an email is never lost if your service is briefly down or slow. Because each email carries a stable `message_id`, a retried delivery hits the unique constraint and becomes a no-op instead of a duplicate row. ## [Trim the payload (optional)](#trim-the-payload-optional) If you only store a handful of columns, a [transform function](https://webhookrelay.com/docs/tutorials/email/database/docs/webhooks/functions/) can shrink the body before it ever reaches your endpoint — your handler then deals with a small, predictable shape: ``` const e = JSON.parse(r.body); r.setBody(JSON.stringify({ message_id: e.message_id, from: e.from, subject: e.subject, body: e.text, received_at: e.date })); ``` This keeps base64 attachment blobs and HTML bodies out of the request when you don't need them. ## [Delivering to a private database](#delivering-to-a-private-database) Your endpoint usually sits next to the database, on a private network. You don't have to expose it. Run the [relay agent](https://webhookrelay.com/docs/tutorials/email/database/webhooks/) alongside the service and Webhook Relay forwards each email to it over an outbound connection — so you can [deliver behind a firewall](https://webhookrelay.com/docs/tutorials/email/database/webhooks/) or to `localhost` with no public IP and no inbound ports open. Durable retries still apply, so a host that's briefly unreachable catches up automatically. ## [Get started](#get-started) Storing inbound mail in a database is one case of a broader pattern: [Email to Webhook](https://webhookrelay.com/docs/tutorials/email/database/email-to-webhook/). Once an email is JSON, you can route it to any endpoint a webhook can reach — including a one-line insert handler. [Create a free Webhook Relay account](https://my.webhookrelay.com/register), add an email input, point it at your endpoint, and start archiving every inbound message in minutes. Did this page help you? --- --- title: Email to Microsoft Teams | WebhookRelay meta: "og: title": "Email to Microsoft Teams" description: Route inbound email into a Microsoft Teams channel. Webhook Relay parses each email to JSON and a transform reshapes it into a Teams card. url: https://webhookrelay.com/docs/tutorials/email/microsoft-teams.md file: /docs/tutorials/email/microsoft-teams.md --- ![Stripes](https://webhookrelay.com/docs/tutorials/email/microsoft-teams/images/stripes.svg) Documentation **Fundamentals** # **Email to Microsoft Teams** Route inbound email into a Microsoft Teams channel. Webhook Relay parses each email to JSON and a transform reshapes it into a Teams card. Plenty of useful signals still arrive as **email** — alert notifications from legacy systems, approval requests, scheduled reports, vendor updates. If your team lives in **Microsoft Teams**, copy-pasting those messages out of a shared inbox is tedious and easy to drop. You want each email to land in a Teams channel automatically. [Webhook Relay](https://webhookrelay.com) does exactly that. It gives you an inbound email address, **parses every message it receives into clean JSON**, and POSTs it to your outputs. Add a small transform that reshapes the parsed email into a Teams card, point the output at your Teams webhook URL, and every email becomes a channel message — no glue server, no inbox polling. > **Build it visually first:** the free [webhook → Slack/Discord/Teams formatter](https://webhookrelay.com/docs/tutorials/email/microsoft-teams/webhook-message-formatter/) lets you paste a sample payload, template the message, preview it live, and copy the exact transform function — for any incoming payload, including a parsed email. ## [How it works](#how-it-works) The flow is a single hop with a transform in the middle: ``` inbound email ──▶ Webhook Relay ──▶ transform to Teams card ──▶ Teams channel (to your addr) (parse to JSON) (serverless function) (message appears) ``` Webhook Relay handles SMTP, parsing, SPF/DKIM/DMARC checks, and attachments for you. By the time the email reaches your transform it's already structured JSON — see [Email to Webhook](https://webhookrelay.com/docs/tutorials/email/microsoft-teams/email-to-webhook/) for the full picture of [receiving emails as webhooks](https://webhookrelay.com/docs/tutorials/email/microsoft-teams/docs/email/). ## [Step 1: Create a bucket](#step-1-create-a-bucket) A bucket is the container that ties an input (where data arrives) to outputs (where it goes). Create one in the dashboard — it's where you'll add the email input and the Teams output. ## [Step 2: Add an email input](#step-2-add-an-email-input) On the bucket, add an **email input**. Webhook Relay generates a unique address: ``` @in.webhookrelay-mail.com ``` Any message sent to that address is parsed and POSTed to the bucket's outputs as JSON. The parsed payload looks like this (see the full [payload reference](https://webhookrelay.com/docs/tutorials/email/microsoft-teams/docs/email/payload/)): ``` { "from": "alerts@example.com", "from_name": "Monitoring", "recipient": "@in.webhookrelay-mail.com", "to": ["@in.webhookrelay-mail.com"], "cc": [], "subject": "Disk usage above 90% on db-01", "date": "2026-06-28T11:59:00Z", "message_id": "<...>", "text": "db-01 root volume is at 92%.", "html": "

db-01 root volume is at 92%.

", "headers": {}, "spf": "pass", "dkim": "pass", "dmarc": "pass", "attachments": [ { "name": "report.csv", "content_type": "text/csv", "size": 1024, "content": "", "truncated": false } ] } ``` ## [Step 3: Add the Teams output](#step-3-add-the-teams-output) Add an output to the bucket and set its destination to your **Teams Incoming Webhook** URL (created via a Power Automate Workflow, or a legacy Office 365 connector). Keep that URL handy — it's where the transformed message gets delivered. ## [Step 4: Attach a transform](#step-4-attach-a-transform) Right now the parsed email would be POSTed to Teams as-is, and Teams would reject it because the shape is wrong. Attach a [transform function](https://webhookrelay.com/docs/tutorials/email/microsoft-teams/docs/webhooks/functions/) to the output to convert the parsed email into a Teams **MessageCard**: ``` const email = JSON.parse(r.body); const card = { "@type": "MessageCard", "@context": "https://schema.org/extensions", summary: email.subject, title: email.subject, text: \`**From:** ${email.from_name || email.from}\n\n${email.text}\` }; r.setBody(JSON.stringify(card)); r.setHeader("Content-Type", "application/json"); ``` The function reads `r.body` (the parsed-email JSON string), pulls out the subject, sender and body, and writes a MessageCard back with `r.setBody()`. Setting `Content-Type: application/json` with `r.setHeader()` ensures Teams accepts it. You can also call `r.setResponseStatus()` if you need to control the response returned to the sender. **A note on Adaptive Cards.** Microsoft is migrating Incoming Webhooks away from Office 365 connectors toward Power Automate Workflows, where the modern format is an **Adaptive Card** wrapped in an `attachments` array. The pattern is identical — parse the email, build the card object, `r.setBody(JSON.stringify(...))` — only the JSON shape differs to match whichever URL you were given. ## [Step 5: Test it](#step-5-test-it) Send a plain email to your `@in.webhookrelay-mail.com` address. Within a second or two the subject and body appear as a card in your Teams channel. If nothing shows up, open the request log in your dashboard to see exactly what was received, transformed, and delivered. ## [Wrapping up](#wrapping-up) That's a complete email-to-Teams bridge: an email input parses each message, a transform reshapes it into a Teams card, and the output delivers it — all in the cloud with nothing to host. The same building blocks cover any destination; see [Email to Webhook](https://webhookrelay.com/docs/tutorials/email/microsoft-teams/email-to-webhook/) for the broader pattern and the [formatter](https://webhookrelay.com/docs/tutorials/email/microsoft-teams/webhook-message-formatter/) to template your card without writing code by hand. Did this page help you? --- --- title: Self-Hosted CI/CD With Drone.io | WebhookRelay meta: "og: title": "Self-Hosted CI/CD With Drone.io" description: A guide/tutorial on how to set up Drone as a self-hosted CI/CD solution for private projects url: https://webhookrelay.com/blog/using-drone-for-simple-selfhosted-ci-cd.md file: /blog/using-drone-for-simple-selfhosted-ci-cd.md --- ![Stripes](https://webhookrelay.com/blog/using-drone-for-simple-selfhosted-ci-cd/images/stripes.svg) # **Setting up simple, self-hosted & fast CI/CD solution with Drone.io** A guide/tutorial on how to set up Drone as a self-hosted CI/CD solution for private projects ![Creating Drone tunnel](https://webhookrelay.com/blog/using-drone-for-simple-selfhosted-ci-cd/images/blog/drone/drone.png) Continuous Integration and Continuous Delivery have been very trendy topics in the DevOps world for the last several years. As more and more organizations strive to improve their test and release processes, we have observed many new tools appear in this ecosystem. All major cloud providers are offering their automation systems ([some are very unsuccessful](https://toxicbakery.github.io/vsts-devops/microsoft-devops-ci/)) so it's getting quite hard to pick the right one that fits your use-case. There are quite a lot of services (Travis, CircleCI, Google Cloud Builder) and self-hosted solutions to build, test and deploy your code, but actually, a few are free, open-source and self-hosted. The most well-know CI/CD tools are Jenkins and GitLab CI (I have recently asked this on [dev.to post](https://dev.to/krusenas/whats-your-favourite-cicd-tool-and-why-52ll)). However, Jenkins has a huge memory footprint since it runs on Tomcat (Java) and the workflows are defined inside Jenkins itself, not in the repository that will be used to run those tests/build jobs. As for GitLab CI, it's very good but requires you to run your own GitLab (which is huge) or to be on gitlab.com (although now you can mirror your Github repos to Gitlab to start builds automatically, however, it adds complexity). You can run your own runner independently though, they seem to be quite reliable. One of the organizations that I am helping with backend development is using Gitlab CI to great success, but as I mentioned before, business pricing and server running costs could be significant. > Disclaimer: I have tried using Drone several years ago and I wasn't happy with it. Not sure whether it was the actual product fault or just documentation but I couldn't achieve what I wanted. This time seems very different. The product is stable, documentation is good and some very good examples are lying inside their own `.drone.yml` file: [https://github.com/drone/drone/blob/master/.drone.yml](https://github.com/drone/drone/blob/master/.drone.yml). Builds are fast and reliable. ## [Enter Drone](#enter-drone) I have looked into many alternatives and decided that at least for me, the best CI/CD solution right now is [Drone](https://drone.io). It's an open source, very lightweight application written in Go that is available as a SaaS or as a self-hosted server. Drone consists of 2 components: - **Server** is responsible for authentication, repository configuration, users, secrets and accepting webhooks. - **Agent** are receiving build jobs and actually run your workflows. Both server and agent are extremely lightweight services and only uses around ~10-15MB RAM. It basically means that you will not even feel that it's running on your laptop, desktop or even a Raspberry Pi. #### [My setup](#my-setup) I am running Drone server on a Raspberry PI while the agent is running locally on my desktop which has lots of RAM and 16 CPU cores. This means that builds are a lot faster than any free public services could offer. What is more, since agents are connecting to the server over the Webhook Relay tunnels, I can deploy them on Kubernetes, spare laptops or other machines running anywhere: ![Drone server on RPI](https://webhookrelay.com/blog/using-drone-for-simple-selfhosted-ci-cd/images/blog/drone/drone-rpi.png) Benefits of such configuration: - No need to configure NAT, Drone server barely uses any resources so it can run on cheap hardware. - Agents are decoupled and can be running on your home/office machines. By connecting to them over the tunnels, agent configuration remains the same, no matter where they are deployed (external cloud provider or internal office). Future plans include transitioning from Raspberry Pi for Drone server to an Intel NUC (or some other low power alternative) and running some additional Drone agents on the same server. ## [Preparing configuration](#preparing-configuration) Initial configuration is very straightforward. First, we create a tunnel on Webhook Relay to get our public subdomain. Then, we use that subdomain to create a Github OAuth app. Once the server and tunneling daemon are running, you can connect/disconnect Drone agents without any hassle. ### [Setting up the tunnel for Github and remote access](#setting-up-the-tunnel-for-github-and-remote-access) I like dogfooding, therefore we are using our own tunnels throughout our stack (CI/CD, monitoring dashboards, various other automations). In this case, Webhook Relay tunnels provide the two most important things: - Access to Drone server no matter where it is (on your laptop, desktop or on some Raspberry Pi), no need to configure a firewall or router to get remote access. - Webhooks from Github will trigger the workflows. Go to your [tunnels page](https://my.webhookrelay.com/tunnels) and create a new tunnel. If you are on a free tier, you will get a generated subdomain, it's fine. ![Creating Drone tunnel](https://webhookrelay.com/blog/using-drone-for-simple-selfhosted-ci-cd/images/blog/drone/drone-tunnel.png) Here: - **name of the tunnel** can be anything, but it has to match whatever you will set to the webhookrelayd container in the docker-compose.yaml. It acts as a filter. - **subdomain** is your public subdomain under `*.webrelay.io`. If you are on a free plan, this will be auto-generated. - **destination** is the address of the drone-server container. Since docker-compose configures virtual networks, it has to match the service name from the docker-compose.yaml. - **crypto type** is available for all paid plans, basically HTTP/TLS. I would recommend subscribing to get encryption! Otherwise, ping me a message and I can set up a free trial. ### [Configuring Github OAuth application](#configuring-github-oauth-application) In this guide, we will be setting up Drone with Github. You can follow [official docs](https://docs.drone.io/installation/github/single-machine/). In short: Create a GitHub OAuth application. The Consumer Key and Consumer Secret are used to authorize access to Github resources. The Authorization callback URL must match the below format and path, and must use your exact server scheme and host. So if your Webhook Relay tunnel address is `my-drone-subdomain.webrelay.io` then the Authorization callback URL will be `https://my-drone-subdomain.webrelay.io/login`. ### [Deploying Drone server & agent](#deploying-drone-server-agent) I have initially started using Drone with both server and agent running on my desktop since it was only for my personal use, it was fine. However, later I detached server and moved it into a Raspberry Pi. The all-in-one configuration looks like: ``` version: '3' services: drone-server: container_name: drone-server image: drone/drone:1.0.0-rc.5 ports: - 8080:80 volumes: - db-data:/var/lib/drone/ restart: always environment: # - DRONE_OPEN=false - DRONE_SERVER_HOST=my-drone-subdomain.webrelay.io # tunnel hostname - DRONE_GITHUB_SERVER=https://github.com - DRONE_ADMIN= - DRONE_GITHUB_CLIENT_ID= - DRONE_GITHUB_CLIENT_SECRET= - DRONE_SERVER_PROTO=http # tunnel adds https on top - DRONE_RPC_SECRET= drone-agent: container_name: drone-agent image: drone/agent:1.0.0-rc.5 command: agent restart: always depends_on: - drone-server volumes: - /var/run/docker.sock:/var/run/docker.sock environment: - DRONE_RPC_SERVER=https://my-drone-subdomain.webrelay.io - DRONE_RPC_SECRET= - DRONE_RUNNER_CAPACITY=2 - DRONE_RUNNER_NAME="local" webhookrelay: container_name: drone-webhookrelay image: webhookrelay/webhookrelayd:latest command: - --mode - tunnel - -t - drone restart: always depends_on: - drone-server environment: - KEY= - SECRET= volumes: db-data: ``` If you need another agent on a different machine, agent-only configuration: ``` version: '3' services: drone-agent: container_name: drone-agent image: drone/agent:1.0.0-rc.5 command: agent restart: always volumes: - /var/run/docker.sock:/var/run/docker.sock environment: - DRONE_RPC_SERVER=https://my-drone-subdomain.webrelay.io - DRONE_RPC_SECRET= - DRONE_RUNNER_CAPACITY=2 - DRONE_RUNNER_NAME="local" ``` Once you have the config: ``` docker-compose up ``` It will ask you to authenticate with your Github credentials. ## [Working with Drone pipelines](#working-with-drone-pipelines) One of the best things about Drone is their pipeline configuration. It seems like after the Drone showed the way, industry followed and now most of the CI/CD services implement the pipelines through the Docker containers. It means that your builds and tests are decoupled from physical hosts and the cleanup happens by design. An example pipeline, that 1. builds 2. unit tests 3. runs end-to-end tests with a Postgres database 4. sends a webhook to Slack 5. builds and pushes an image to DockerHub looks like this: ``` kind: pipeline name: default workspace: base: /go path: src/github.com/webhookrelay/app steps: - name: build image: golang:1.11.4 commands: - cd cmd/app && go build - name: unit-test image: golang:1.11.4 commands: - go test -v \`go list ./... \` - name: end-to-end image: golang:1.11.4 environment: POSTGRES_HOST: database POSTGRES_USER: pguser POSTGRES_PASSWORD: pgpass POSTGRES_DB: pgdb commands: - make install-transponder - make e2e - name: slack image: plugins/slack settings: webhook: from_secret: slack_url channel: general username: drone icon_url: https://i.pinimg.com/originals/51/29/a4/5129a48ddad9e8408d2757dd10eb836f.jpg - name: docker image: plugins/docker settings: repo: namespace/repo auto_tag: true username: from_secret: docker_username password: from_secret: docker_password services: - name: database image: postgres environment: POSTGRES_USER: pguser POSTGRES_PASSWORD: pgpass POSTGRES_DB: pgdb ``` By always using containers to run your pipelines, you will make your builds and tests truly portable. Whenever you push to Github, a webhook will hit Drone and it will start a build: ![Drone pipeline](https://webhookrelay.com/blog/using-drone-for-simple-selfhosted-ci-cd/images/blog/drone/build-complete.png) ## [Tips and tricks](#tips-and-tricks) I would also like to share some very useful commands. To install `drone` CLI, follow [official docs](https://docs.drone.io/cli/install/). #### [Executing pipelines directly](#executing-pipelines-directly) You can also run pipelines directly with the Drone CLI: ``` drone exec --secret-file drone_secrets.yaml .drone.yml ``` A template for Drone secrets: ``` slack_url: https://hooks.slack.com/services/xxxxxxxxxxxx ``` #### [Adding secrets through CLI](#adding-secrets-through-cli) You can either add secrets through the web UI or use `drone` directly from your terminal: ``` drone secret add -repository username/repository-name --name foo --data bar --allow-pull-request ``` ## [Wrapping up](#wrapping-up) I have used extensively Jenkins, Gitlab runners, CircleCI and Google Cloud Builder. I guess I still like a lot Cloud Builder for GKE related things as it builds images fast and authenticates to GCR registry without any additional work. However, you do get stuck with their pipeline configuration files and can't really move to any other vendor. As for Jenkins, it's just hard to imagine running it efficiently and smaller servers. Drone, at least for me, is an ideal example of a modern service that focuses on efficiency and doing one thing right. --- --- title: Free Standard Webhooks Signature Verifier | WebhookRelay meta: "og: title": "Free Standard Webhooks Signature Verifier" description: Free Standard Webhooks signature verifier. Validate the webhook-signature header used by the Standard Webhooks spec. No signup, runs in your browser. url: https://webhookrelay.com/verify-standard-webhooks-signature.md file: /verify-standard-webhooks-signature.md --- # **Verify Standard Webhooks Signatures** Standard Webhooks (used by Svix, OpenAI, Anthropic and many others) signs the string `{webhook-id}.{webhook-timestamp}.{body}` with HMAC-SHA256. The base64 result is sent (prefixed `v1,`) in `webhook-signature`. The signing secret is base64 after its `whsec_` prefix and is decoded before use. Paste the id, timestamp, body, secret and signature. **Raw request body (payload)** **Webhook ID **(webhook-id) **Timestamp** (webhook-timestamp) **Signing secret** **Computed signature** **Signature to verify **(webhook-signature) **Paste the webhook-signature value above to compare** Everything runs in your browser — the payload and secret never leave this page. Want to verify a different provider? See the [webhook signature verifier hub](https://webhookrelay.com/verify-standard-webhooks-signature/verify-webhook-signature/) or the generic [HMAC generator](https://webhookrelay.com/verify-standard-webhooks-signature/hmac-verification/). ## **How Standard Webhooks signs webhooks** 1. Read `webhook-id`, `webhook-timestamp` and `webhook-signature` from the headers. 2. Build the signed content `{id}.{timestamp}.{raw body}`. 3. Base64-decode your secret (the part after `whsec_`) and use those bytes as the HMAC-SHA256 key. 4. Base64-encode the digest, prefix `v1,` and constant-time compare against any signature in `webhook-signature`. **References & official docs:** - [Standard Webhooks](https://www.standardwebhooks.com/) - [Standard Webhooks — Specification](https://github.com/standard-webhooks/standard-webhooks/blob/main/spec/standard-webhooks.md) ## **Verify Standard Webhooks signatures in code** **Node.js** ``` const { Webhook } = require('standardwebhooks'); const wh = new Webhook(process.env.WEBHOOK_SECRET); // whsec_... const payload = wh.verify(rawBody, { 'webhook-id': req.headers['webhook-id'], 'webhook-timestamp': req.headers['webhook-timestamp'], 'webhook-signature': req.headers['webhook-signature'], }); ``` **Python** ``` from standardwebhooks import Webhook wh = Webhook(webhook_secret) # whsec_... payload = wh.verify(raw_body, { "webhook-id": headers["webhook-id"], "webhook-timestamp": headers["webhook-timestamp"], "webhook-signature": headers["webhook-signature"], }) ``` ## **Frequently asked questions**
**What is Standard Webhooks?** An open specification (backed by Svix and a steering committee from Zapier, Twilio, ngrok, Supabase and others) for signing and sending webhooks consistently. Providers like OpenAI, Anthropic and Google Gemini use it.
**How is the secret used?** The signing secret is base64-encoded with a `whsec_` prefix. Strip the prefix, base64-decode the rest to raw bytes, and use those bytes as the HMAC-SHA256 key — not the literal string.
**How is this different from Stripe?** Both use HMAC-SHA256 with a timestamp, but Standard Webhooks also signs a unique message id, outputs base64 (not hex), and decodes its secret from base64 first.
## **Verify other providers** [![Stripe logo](https://webhookrelay.com/verify-standard-webhooks-signature/images/landing/logos/stripe.svg)Stripe](https://webhookrelay.com/verify-standard-webhooks-signature/verify-stripe-webhook-signature/) [![GitHub logo](https://webhookrelay.com/verify-standard-webhooks-signature/images/landing/logos/github.svg) GitHub](https://webhookrelay.com/verify-standard-webhooks-signature/verify-github-webhook-signature/) [![Shopify logo](https://webhookrelay.com/verify-standard-webhooks-signature/images/landing/logos/shopify.svg) Shopify](https://webhookrelay.com/verify-standard-webhooks-signature/verify-shopify-webhook-signature/) [![Slack logo](https://webhookrelay.com/verify-standard-webhooks-signature/images/landing/logos/slack.svg) Slack](https://webhookrelay.com/verify-standard-webhooks-signature/verify-slack-webhook-signature/) [![Twilio logo](https://webhookrelay.com/verify-standard-webhooks-signature/images/landing/logos/twilio.svg) Twilio](https://webhookrelay.com/verify-standard-webhooks-signature/verify-twilio-webhook-signature/) [![Square logo](https://webhookrelay.com/verify-standard-webhooks-signature/images/landing/logos/square.svg) Square](https://webhookrelay.com/verify-standard-webhooks-signature/verify-square-webhook-signature/) [**L** LINE](https://webhookrelay.com/verify-standard-webhooks-signature/verify-line-webhook-signature/) [**A** Airwallex](https://webhookrelay.com/verify-standard-webhooks-signature/verify-airwallex-webhook-signature/) [**M** MyFatoorah](https://webhookrelay.com/verify-standard-webhooks-signature/verify-myfatoorah-webhook-signature/) [![Zendesk logo](https://webhookrelay.com/verify-standard-webhooks-signature/images/landing/logos/zendesk.svg) Zendesk](https://webhookrelay.com/verify-standard-webhooks-signature/verify-zendesk-webhook-signature/) [![DocuSign logo](https://webhookrelay.com/verify-standard-webhooks-signature/images/landing/logos/docusign.svg) DocuSign](https://webhookrelay.com/verify-standard-webhooks-signature/verify-docusign-webhook-signature/) [**K** Klarna](https://webhookrelay.com/verify-standard-webhooks-signature/verify-klarna-webhook-signature/) [**A** Attio](https://webhookrelay.com/verify-standard-webhooks-signature/verify-attio-webhook-signature/) [HMAC Generator](https://webhookrelay.com/verify-standard-webhooks-signature/hmac-verification/) [CloudEvents Validator](https://webhookrelay.com/verify-standard-webhooks-signature/cloudevents-validator/) [Webhook Tester (Bin)](https://webhookrelay.com/verify-standard-webhooks-signature/webhook-bin/) Receiving Standard Webhooks webhooks on a server behind a firewall or on localhost? Webhook Relay can [forward them to your internal service](https://webhookrelay.com/verify-standard-webhooks-signature/webhooks/) and even [verify or transform them](https://webhookrelay.com/verify-standard-webhooks-signature/features/transform-webhooks/) before delivery. --- --- title: Free Slack Webhook Signature Verifier | WebhookRelay meta: "og: title": "Free Slack Webhook Signature Verifier" description: Free Slack webhook signature verifier. Validate the X-Slack-Signature header against your app's signing secret. No signup, runs in your browser. url: https://webhookrelay.com/verify-slack-webhook-signature.md file: /verify-slack-webhook-signature.md --- # **Verify Slack Webhook Signatures** Slack signs requests by building the string `v0:{timestamp}:{body}` and computing HMAC-SHA256 with your app's signing secret. The result is sent (prefixed `v0=`) in `X-Slack-Signature`, with the timestamp in `X-Slack-Request-Timestamp`. Paste the raw body, secret, timestamp and signature. **Raw request body (payload)** **Timestamp** (X-Slack-Request-Timestamp) **Signing secret** **Computed signature** **Signature to verify **(X-Slack-Signature) **Paste the X-Slack-Signature value above to compare** Everything runs in your browser — the payload and secret never leave this page. Want to verify a different provider? See the [webhook signature verifier hub](https://webhookrelay.com/verify-slack-webhook-signature/verify-webhook-signature/) or the generic [HMAC generator](https://webhookrelay.com/verify-slack-webhook-signature/hmac-verification/). ## **How Slack signs webhooks** 1. Build the base string `v0:{X-Slack-Request-Timestamp}:{raw body}`. 2. Compute HMAC-SHA256 using your signing secret as the key, hex-encoded. 3. Prefix with `v0=` and compare to `X-Slack-Signature` with a constant-time check. 4. Reject requests whose timestamp is more than ~5 minutes old to prevent replays. **References & official docs:** - [Slack — Verifying requests from Slack](https://api.slack.com/authentication/verifying-requests-from-slack) ## **Verify Slack signatures in code** **Node.js** ``` const crypto = require('crypto'); const ts = req.headers['x-slack-request-timestamp']; const base = \`v0:${ts}:${rawBody}\`; const sig = 'v0=' + crypto .createHmac('sha256', process.env.SLACK_SIGNING_SECRET) .update(base).digest('hex'); const valid = crypto.timingSafeEqual( Buffer.from(sig), Buffer.from(req.headers['x-slack-signature'])); ``` **Python** ``` import hmac, hashlib ts = request.headers['X-Slack-Request-Timestamp'] base = f"v0:{ts}:{raw_body}" sig = 'v0=' + hmac.new( signing_secret.encode(), base.encode(), hashlib.sha256).hexdigest() valid = hmac.compare_digest(sig, request.headers['X-Slack-Signature']) ``` ## **Frequently asked questions**
**Where is the Slack signing secret?** In your app's settings at api.slack.com under "Basic Information" → "App Credentials" → "Signing Secret". It is different from a bot or OAuth token.
**Why include the timestamp?** Signing `v0:{timestamp}:{body}` binds the signature to a moment in time, so you can reject old requests and stop attackers replaying a captured payload.
## **Verify other providers** [![Stripe logo](https://webhookrelay.com/verify-slack-webhook-signature/images/landing/logos/stripe.svg)Stripe](https://webhookrelay.com/verify-slack-webhook-signature/verify-stripe-webhook-signature/) [![GitHub logo](https://webhookrelay.com/verify-slack-webhook-signature/images/landing/logos/github.svg) GitHub](https://webhookrelay.com/verify-slack-webhook-signature/verify-github-webhook-signature/) [![Shopify logo](https://webhookrelay.com/verify-slack-webhook-signature/images/landing/logos/shopify.svg) Shopify](https://webhookrelay.com/verify-slack-webhook-signature/verify-shopify-webhook-signature/) [![Twilio logo](https://webhookrelay.com/verify-slack-webhook-signature/images/landing/logos/twilio.svg) Twilio](https://webhookrelay.com/verify-slack-webhook-signature/verify-twilio-webhook-signature/) [**S** Standard Webhooks](https://webhookrelay.com/verify-slack-webhook-signature/verify-standard-webhooks-signature/) [![Square logo](https://webhookrelay.com/verify-slack-webhook-signature/images/landing/logos/square.svg) Square](https://webhookrelay.com/verify-slack-webhook-signature/verify-square-webhook-signature/) [**L** LINE](https://webhookrelay.com/verify-slack-webhook-signature/verify-line-webhook-signature/) [**A** Airwallex](https://webhookrelay.com/verify-slack-webhook-signature/verify-airwallex-webhook-signature/) [**M** MyFatoorah](https://webhookrelay.com/verify-slack-webhook-signature/verify-myfatoorah-webhook-signature/) [![Zendesk logo](https://webhookrelay.com/verify-slack-webhook-signature/images/landing/logos/zendesk.svg) Zendesk](https://webhookrelay.com/verify-slack-webhook-signature/verify-zendesk-webhook-signature/) [![DocuSign logo](https://webhookrelay.com/verify-slack-webhook-signature/images/landing/logos/docusign.svg) DocuSign](https://webhookrelay.com/verify-slack-webhook-signature/verify-docusign-webhook-signature/) [**K** Klarna](https://webhookrelay.com/verify-slack-webhook-signature/verify-klarna-webhook-signature/) [**A** Attio](https://webhookrelay.com/verify-slack-webhook-signature/verify-attio-webhook-signature/) [HMAC Generator](https://webhookrelay.com/verify-slack-webhook-signature/hmac-verification/) [CloudEvents Validator](https://webhookrelay.com/verify-slack-webhook-signature/cloudevents-validator/) [Webhook Tester (Bin)](https://webhookrelay.com/verify-slack-webhook-signature/webhook-bin/) Receiving Slack webhooks on a server behind a firewall or on localhost? Webhook Relay can [forward them to your internal service](https://webhookrelay.com/verify-slack-webhook-signature/webhooks/) and even [verify or transform them](https://webhookrelay.com/verify-slack-webhook-signature/features/transform-webhooks/) before delivery. --- --- title: Free Stripe Webhook Signature Verifier | WebhookRelay meta: "og: title": "Free Stripe Webhook Signature Verifier" description: Free Stripe webhook signature verifier. Validate the Stripe-Signature header against your endpoint signing secret. No signup, runs in your browser. url: https://webhookrelay.com/verify-stripe-webhook-signature.md file: /verify-stripe-webhook-signature.md --- # **Verify Stripe Webhook Signatures** Stripe signs every webhook with an HMAC-SHA256 over a timestamped payload and sends it in the `Stripe-Signature` header (e.g. `t=1614265330,v1=…`). Verifying it prevents spoofed events and replay attacks. Paste the raw request body, your endpoint signing secret and the header below. **Raw request body (payload)** **Timestamp** **Endpoint signing secret** **Computed signature** **Signature to verify **(Stripe-Signature) **Paste the Stripe-Signature value above to compare** Everything runs in your browser — the payload and secret never leave this page. Want to verify a different provider? See the [webhook signature verifier hub](https://webhookrelay.com/verify-stripe-webhook-signature/verify-webhook-signature/) or the generic [HMAC generator](https://webhookrelay.com/verify-stripe-webhook-signature/hmac-verification/). ## **How Stripe signs webhooks** 1. Read the `t` (timestamp) and `v1` (signature) values from the `Stripe-Signature` header. 2. Build the signed payload string: the timestamp, a literal `.`, then the **raw** request body. 3. Compute HMAC-SHA256 of that string using your endpoint signing secret (`whsec_…`) as the key, hex-encoded. 4. Compare it to `v1` with a constant-time check, and reject events whose timestamp is too old. **References & official docs:** - [Stripe — Verify webhook signatures](https://docs.stripe.com/webhooks/signature) - [Stripe — Webhooks overview](https://docs.stripe.com/webhooks) ## **Verify Stripe signatures in code** **Node.js** ``` const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); // req.body must be the RAW body (Buffer/string), not parsed JSON. const sig = req.headers['stripe-signature']; try { const event = stripe.webhooks.constructEvent( req.body, sig, process.env.STRIPE_WEBHOOK_SECRET); // event is verified — handle it } catch (err) { return res.status(400).send(\`Webhook Error: ${err.message}\`); } ``` **Python** ``` import stripe sig_header = request.headers['Stripe-Signature'] try: event = stripe.Webhook.construct_event( request.data, sig_header, endpoint_secret) # event is verified — handle it except stripe.error.SignatureVerificationError: return '', 400 ``` ## **Frequently asked questions**
**What is the Stripe-Signature header?** It is the header Stripe adds to every webhook, formatted like `t=1614265330,v1=5257a8…`. `t` is the unix timestamp and `v1` is the HMAC-SHA256 signature you verify.
**Why does verification fail even with the right secret?** Almost always because the body was re-serialized. Stripe signs the exact raw bytes it sent, so you must verify against the unparsed request body — not `JSON.stringify(req.body)`.
**Which secret do I use?** The endpoint signing secret that starts with `whsec_`, shown in the Stripe Dashboard under Developers → Webhooks for that specific endpoint. It is not your API key.
## **Verify other providers** [![GitHub logo](https://webhookrelay.com/verify-stripe-webhook-signature/images/landing/logos/github.svg)GitHub](https://webhookrelay.com/verify-stripe-webhook-signature/verify-github-webhook-signature/) [![Shopify logo](https://webhookrelay.com/verify-stripe-webhook-signature/images/landing/logos/shopify.svg) Shopify](https://webhookrelay.com/verify-stripe-webhook-signature/verify-shopify-webhook-signature/) [![Slack logo](https://webhookrelay.com/verify-stripe-webhook-signature/images/landing/logos/slack.svg) Slack](https://webhookrelay.com/verify-stripe-webhook-signature/verify-slack-webhook-signature/) [![Twilio logo](https://webhookrelay.com/verify-stripe-webhook-signature/images/landing/logos/twilio.svg) Twilio](https://webhookrelay.com/verify-stripe-webhook-signature/verify-twilio-webhook-signature/) [**S** Standard Webhooks](https://webhookrelay.com/verify-stripe-webhook-signature/verify-standard-webhooks-signature/) [![Square logo](https://webhookrelay.com/verify-stripe-webhook-signature/images/landing/logos/square.svg) Square](https://webhookrelay.com/verify-stripe-webhook-signature/verify-square-webhook-signature/) [**L** LINE](https://webhookrelay.com/verify-stripe-webhook-signature/verify-line-webhook-signature/) [**A** Airwallex](https://webhookrelay.com/verify-stripe-webhook-signature/verify-airwallex-webhook-signature/) [**M** MyFatoorah](https://webhookrelay.com/verify-stripe-webhook-signature/verify-myfatoorah-webhook-signature/) [![Zendesk logo](https://webhookrelay.com/verify-stripe-webhook-signature/images/landing/logos/zendesk.svg) Zendesk](https://webhookrelay.com/verify-stripe-webhook-signature/verify-zendesk-webhook-signature/) [![DocuSign logo](https://webhookrelay.com/verify-stripe-webhook-signature/images/landing/logos/docusign.svg) DocuSign](https://webhookrelay.com/verify-stripe-webhook-signature/verify-docusign-webhook-signature/) [**K** Klarna](https://webhookrelay.com/verify-stripe-webhook-signature/verify-klarna-webhook-signature/) [**A** Attio](https://webhookrelay.com/verify-stripe-webhook-signature/verify-attio-webhook-signature/) [HMAC Generator](https://webhookrelay.com/verify-stripe-webhook-signature/hmac-verification/) [CloudEvents Validator](https://webhookrelay.com/verify-stripe-webhook-signature/cloudevents-validator/) [Webhook Tester (Bin)](https://webhookrelay.com/verify-stripe-webhook-signature/webhook-bin/) Receiving Stripe webhooks on a server behind a firewall or on localhost? Webhook Relay can [forward them to your internal service](https://webhookrelay.com/verify-stripe-webhook-signature/webhooks/) and even [verify or transform them](https://webhookrelay.com/verify-stripe-webhook-signature/features/transform-webhooks/) before delivery. --- --- title: Free Square Webhook Signature Verifier | WebhookRelay meta: "og: title": "Free Square Webhook Signature Verifier" description: Free Square webhook signature verifier. Validate the x-square-hmacsha256-signature header against your subscription signature key. No signup required. url: https://webhookrelay.com/verify-square-webhook-signature.md file: /verify-square-webhook-signature.md --- # **Verify Square Webhook Signatures** Square signs each webhook with HMAC-SHA256 over your **notification URL concatenated with the raw request body** and sends the base64 result in the `x-square-hmacsha256-signature` header. The key is the signature key of that webhook subscription. Enter the notification URL exactly as configured, the raw body, the key and the header value. **Notification URL (your webhook endpoint)** **Raw request body (payload)** **Webhook signature key** **Computed signature** **Signature to verify **(x-square-hmacsha256-signature) **Paste the x-square-hmacsha256-signature value above to compare** Everything runs in your browser — the payload and secret never leave this page. Want to verify a different provider? See the [webhook signature verifier hub](https://webhookrelay.com/verify-square-webhook-signature/verify-webhook-signature/) or the generic [HMAC generator](https://webhookrelay.com/verify-square-webhook-signature/hmac-verification/). ## **How Square signs webhooks** 1. Take the full notification URL exactly as configured in your subscription — scheme, host, path and any trailing slash must match. 2. Concatenate that URL directly with the **raw** request body: URL first, body second, with no separator. 3. Compute HMAC-SHA256 of the combined string using your subscription's signature key as the key. 4. Base64-encode the digest and constant-time compare it to `x-square-hmacsha256-signature`. **References & official docs:** - [Square — Validate an event notification](https://developer.squareup.com/docs/webhooks/step3validate) - [Square — Webhooks overview](https://developer.squareup.com/docs/webhooks/overview) ## **Verify Square signatures in code** **Node.js** ``` const crypto = require('crypto'); // notificationUrl must match the subscription URL exactly (incl. trailing slash). const expected = crypto .createHmac('sha256', process.env.SQUARE_SIGNATURE_KEY) .update(notificationUrl + rawBody) // URL first, then the raw body .digest('base64'); const received = req.headers['x-square-hmacsha256-signature']; const valid = crypto.timingSafeEqual( Buffer.from(expected), Buffer.from(received)); ``` **Python** ``` import hmac, hashlib, base64 payload = notification_url.encode() + raw_body # URL + raw body, no separator expected = base64.b64encode(hmac.new( signature_key.encode(), payload, hashlib.sha256).digest()).decode() received = request.headers['x-square-hmacsha256-signature'] valid = hmac.compare_digest(expected, received) ``` ## **Frequently asked questions**
**Which key signs Square webhooks?** The signature key for that specific webhook subscription, shown in the Square Developer Dashboard under Webhooks → Subscriptions. Each subscription has its own key, and it is used as a raw string — not base64-decoded.
**Why does my Square signature never match?** The most common cause is the notification URL not matching exactly: a missing or extra trailing slash, `http` vs `https`, or a proxy rewriting the host all change the result. Sign with the URL you configured in the subscription, and use the raw request body.
**Does Square sign a timestamp?** No. The modern HMAC-SHA256 scheme signs only the URL plus body, so there is no built-in replay protection — rely on HTTPS and idempotent handling. The legacy `x-square-signature` header (HMAC-SHA1) is deprecated; verify `x-square-hmacsha256-signature` instead.
## **Verify other providers** [![Stripe logo](https://webhookrelay.com/verify-square-webhook-signature/images/landing/logos/stripe.svg)Stripe](https://webhookrelay.com/verify-square-webhook-signature/verify-stripe-webhook-signature/) [![GitHub logo](https://webhookrelay.com/verify-square-webhook-signature/images/landing/logos/github.svg) GitHub](https://webhookrelay.com/verify-square-webhook-signature/verify-github-webhook-signature/) [![Shopify logo](https://webhookrelay.com/verify-square-webhook-signature/images/landing/logos/shopify.svg) Shopify](https://webhookrelay.com/verify-square-webhook-signature/verify-shopify-webhook-signature/) [![Slack logo](https://webhookrelay.com/verify-square-webhook-signature/images/landing/logos/slack.svg) Slack](https://webhookrelay.com/verify-square-webhook-signature/verify-slack-webhook-signature/) [![Twilio logo](https://webhookrelay.com/verify-square-webhook-signature/images/landing/logos/twilio.svg) Twilio](https://webhookrelay.com/verify-square-webhook-signature/verify-twilio-webhook-signature/) [**S** Standard Webhooks](https://webhookrelay.com/verify-square-webhook-signature/verify-standard-webhooks-signature/) [**L** LINE](https://webhookrelay.com/verify-square-webhook-signature/verify-line-webhook-signature/) [**A** Airwallex](https://webhookrelay.com/verify-square-webhook-signature/verify-airwallex-webhook-signature/) [**M** MyFatoorah](https://webhookrelay.com/verify-square-webhook-signature/verify-myfatoorah-webhook-signature/) [![Zendesk logo](https://webhookrelay.com/verify-square-webhook-signature/images/landing/logos/zendesk.svg) Zendesk](https://webhookrelay.com/verify-square-webhook-signature/verify-zendesk-webhook-signature/) [![DocuSign logo](https://webhookrelay.com/verify-square-webhook-signature/images/landing/logos/docusign.svg) DocuSign](https://webhookrelay.com/verify-square-webhook-signature/verify-docusign-webhook-signature/) [**K** Klarna](https://webhookrelay.com/verify-square-webhook-signature/verify-klarna-webhook-signature/) [**A** Attio](https://webhookrelay.com/verify-square-webhook-signature/verify-attio-webhook-signature/) [HMAC Generator](https://webhookrelay.com/verify-square-webhook-signature/hmac-verification/) [CloudEvents Validator](https://webhookrelay.com/verify-square-webhook-signature/cloudevents-validator/) [Webhook Tester (Bin)](https://webhookrelay.com/verify-square-webhook-signature/webhook-bin/) Receiving Square webhooks on a server behind a firewall or on localhost? Webhook Relay can [forward them to your internal service](https://webhookrelay.com/verify-square-webhook-signature/webhooks/) and even [verify or transform them](https://webhookrelay.com/verify-square-webhook-signature/features/transform-webhooks/) before delivery. --- --- title: Free Twilio Webhook Signature Verifier | WebhookRelay meta: "og: title": "Free Twilio Webhook Signature Verifier" description: Free Twilio webhook signature verifier. Validate the X-Twilio-Signature header for form-encoded webhook callbacks. No signup, runs in your browser. url: https://webhookrelay.com/verify-twilio-webhook-signature.md file: /verify-twilio-webhook-signature.md --- # **Verify Twilio Webhook Signatures** Twilio signs requests differently from most providers: it takes your exact webhook URL, appends every POST parameter sorted alphabetically (key immediately followed by value), and computes HMAC-SHA1 with your Auth Token, base64-encoded. The result is sent in `X-Twilio-Signature`. Enter the URL, the POST parameters and your Auth Token. **Request URL** **POST parameters (one **`key=value`** per line)** **Auth token** **Computed signature** **Signature to verify **(X-Twilio-Signature) **Paste the X-Twilio-Signature value above to compare** Everything runs in your browser — the payload and secret never leave this page. Want to verify a different provider? See the [webhook signature verifier hub](https://webhookrelay.com/verify-twilio-webhook-signature/verify-webhook-signature/) or the generic [HMAC generator](https://webhookrelay.com/verify-twilio-webhook-signature/hmac-verification/). ## **How Twilio signs webhooks** 1. Start with the full request URL exactly as configured (including https:// and any query string). 2. Sort the POST parameters alphabetically by name and append each name immediately followed by its value. 3. Compute HMAC-SHA1 of that string using your Auth Token as the key, base64-encoded. 4. Compare to `X-Twilio-Signature` with a constant-time check. **References & official docs:** - [Twilio — Security: validating signatures](https://www.twilio.com/docs/usage/security) - [Twilio — Webhooks security](https://www.twilio.com/docs/usage/webhooks/webhooks-security) ## **Verify Twilio signatures in code** **Node.js** ``` const twilio = require('twilio'); const valid = twilio.validateRequest( process.env.TWILIO_AUTH_TOKEN, req.headers['x-twilio-signature'], url, // the exact URL Twilio requested req.body); // parsed POST params ``` **Python** ``` from twilio.request_validator import RequestValidator validator = RequestValidator(auth_token) valid = validator.validate( url, # the exact URL Twilio requested request.form, # POST params request.headers['X-Twilio-Signature']) ``` ## **Frequently asked questions**
**Why is Twilio different from Stripe or GitHub?** Twilio signs the URL plus the sorted POST parameters (not the raw JSON body) with HMAC-SHA1. This tool covers that standard form-encoded callback scheme; JSON bodies and the newer public-key signatures work differently.
**What key does Twilio use?** Your account Auth Token, found in the Twilio Console. Rotate it carefully — it signs every request from Twilio.
## **Verify other providers** [![Stripe logo](https://webhookrelay.com/verify-twilio-webhook-signature/images/landing/logos/stripe.svg)Stripe](https://webhookrelay.com/verify-twilio-webhook-signature/verify-stripe-webhook-signature/) [![GitHub logo](https://webhookrelay.com/verify-twilio-webhook-signature/images/landing/logos/github.svg) GitHub](https://webhookrelay.com/verify-twilio-webhook-signature/verify-github-webhook-signature/) [![Shopify logo](https://webhookrelay.com/verify-twilio-webhook-signature/images/landing/logos/shopify.svg) Shopify](https://webhookrelay.com/verify-twilio-webhook-signature/verify-shopify-webhook-signature/) [![Slack logo](https://webhookrelay.com/verify-twilio-webhook-signature/images/landing/logos/slack.svg) Slack](https://webhookrelay.com/verify-twilio-webhook-signature/verify-slack-webhook-signature/) [**S** Standard Webhooks](https://webhookrelay.com/verify-twilio-webhook-signature/verify-standard-webhooks-signature/) [![Square logo](https://webhookrelay.com/verify-twilio-webhook-signature/images/landing/logos/square.svg) Square](https://webhookrelay.com/verify-twilio-webhook-signature/verify-square-webhook-signature/) [**L** LINE](https://webhookrelay.com/verify-twilio-webhook-signature/verify-line-webhook-signature/) [**A** Airwallex](https://webhookrelay.com/verify-twilio-webhook-signature/verify-airwallex-webhook-signature/) [**M** MyFatoorah](https://webhookrelay.com/verify-twilio-webhook-signature/verify-myfatoorah-webhook-signature/) [![Zendesk logo](https://webhookrelay.com/verify-twilio-webhook-signature/images/landing/logos/zendesk.svg) Zendesk](https://webhookrelay.com/verify-twilio-webhook-signature/verify-zendesk-webhook-signature/) [![DocuSign logo](https://webhookrelay.com/verify-twilio-webhook-signature/images/landing/logos/docusign.svg) DocuSign](https://webhookrelay.com/verify-twilio-webhook-signature/verify-docusign-webhook-signature/) [**K** Klarna](https://webhookrelay.com/verify-twilio-webhook-signature/verify-klarna-webhook-signature/) [**A** Attio](https://webhookrelay.com/verify-twilio-webhook-signature/verify-attio-webhook-signature/) [HMAC Generator](https://webhookrelay.com/verify-twilio-webhook-signature/hmac-verification/) [CloudEvents Validator](https://webhookrelay.com/verify-twilio-webhook-signature/cloudevents-validator/) [Webhook Tester (Bin)](https://webhookrelay.com/verify-twilio-webhook-signature/webhook-bin/) Receiving Twilio webhooks on a server behind a firewall or on localhost? Webhook Relay can [forward them to your internal service](https://webhookrelay.com/verify-twilio-webhook-signature/webhooks/) and even [verify or transform them](https://webhookrelay.com/verify-twilio-webhook-signature/features/transform-webhooks/) before delivery. --- --- title: Free Zendesk Webhook Signature Verifier | WebhookRelay meta: "og: title": "Free Zendesk Webhook Signature Verifier" description: Free Zendesk webhook signature verifier. Validate the X-Zendesk-Webhook-Signature header against your webhook signing secret. No signup, runs in your browser. url: https://webhookrelay.com/verify-zendesk-webhook-signature.md file: /verify-zendesk-webhook-signature.md --- # **Verify Zendesk Webhook Signatures** Zendesk signs every webhook with HMAC-SHA256 over the delivery timestamp concatenated with the raw body, base64-encodes it and sends it in the `X-Zendesk-Webhook-Signature` header, with the timestamp in `X-Zendesk-Webhook-Signature-Timestamp`. Paste the raw body, the timestamp header value and your signing secret below. **Raw request body (payload)** Paste the **raw** request body exactly as received. Some Zendesk deliveries (e.g. test pings) have an empty body — the timestamp alone is signed then. **Timestamp** (X-Zendesk-Webhook-Signature-Timestamp) **Webhook signing secret** **Computed signature** **Signature to verify **(X-Zendesk-Webhook-Signature) **Paste the X-Zendesk-Webhook-Signature value above to compare** Everything runs in your browser — the payload and secret never leave this page. Want to verify a different provider? See the [webhook signature verifier hub](https://webhookrelay.com/verify-zendesk-webhook-signature/verify-webhook-signature/) or the generic [HMAC generator](https://webhookrelay.com/verify-zendesk-webhook-signature/hmac-verification/). ## **How Zendesk signs webhooks** 1. Read `X-Zendesk-Webhook-Signature` (the signature) and `X-Zendesk-Webhook-Signature-Timestamp` (an ISO-8601 timestamp) from the request. 2. Build the signed string: the timestamp value immediately followed by the **raw** request body (no separator). 3. Compute HMAC-SHA256 of that string with your webhook signing secret as the key, then base64-encode the digest. 4. Compare it to the header with a constant-time check, and reject deliveries whose timestamp is too old to defend against replays. **References & official docs:** - [Zendesk — Verifying webhook authenticity](https://developer.zendesk.com/documentation/webhooks/verifying/) - [Zendesk — Webhooks overview](https://developer.zendesk.com/documentation/webhooks/) ## **Verify Zendesk signatures in code** **Node.js** ``` const crypto = require('crypto'); // body must be the RAW request body (string/Buffer), not parsed JSON. function isValidZendeskWebhook(body, signature, timestamp, secret) { const expected = crypto .createHmac('sha256', secret) .update(timestamp + body) .digest('base64'); return crypto.timingSafeEqual( Buffer.from(expected), Buffer.from(signature)); } const ok = isValidZendeskWebhook( rawBody, req.headers['x-zendesk-webhook-signature'], req.headers['x-zendesk-webhook-signature-timestamp'], process.env.ZENDESK_SIGNING_SECRET); ``` **Python** ``` import base64, hashlib, hmac def is_valid_zendesk_webhook(body: bytes, signature: str, timestamp: str, secret: str) -> bool: digest = hmac.new(secret.encode(), timestamp.encode() + body, hashlib.sha256).digest() expected = base64.b64encode(digest).decode() return hmac.compare_digest(expected, signature) ``` ## **Frequently asked questions**
**Where do I find the Zendesk webhook signing secret?** In Admin Center open the webhook's details page and click _Reveal secret_, or call `GET /api/v2/webhooks/{webhook_id}/signing_secret`. Each webhook has its own secret.
**What exactly does Zendesk sign?** The value of `X-Zendesk-Webhook-Signature-Timestamp` concatenated directly with the raw request body — no separator between them. Requests without a body (some test deliveries) sign the timestamp alone.
**Why does my verification fail with the right secret?** Usually the body was re-serialized before hashing (parse-then-stringify changes bytes), or the timestamp header was omitted from the signed string. Always verify against the raw bytes Zendesk sent, prefixed with the timestamp value.
## **Verify other providers** [![Stripe logo](https://webhookrelay.com/verify-zendesk-webhook-signature/images/landing/logos/stripe.svg)Stripe](https://webhookrelay.com/verify-zendesk-webhook-signature/verify-stripe-webhook-signature/) [![GitHub logo](https://webhookrelay.com/verify-zendesk-webhook-signature/images/landing/logos/github.svg) GitHub](https://webhookrelay.com/verify-zendesk-webhook-signature/verify-github-webhook-signature/) [![Shopify logo](https://webhookrelay.com/verify-zendesk-webhook-signature/images/landing/logos/shopify.svg) Shopify](https://webhookrelay.com/verify-zendesk-webhook-signature/verify-shopify-webhook-signature/) [![Slack logo](https://webhookrelay.com/verify-zendesk-webhook-signature/images/landing/logos/slack.svg) Slack](https://webhookrelay.com/verify-zendesk-webhook-signature/verify-slack-webhook-signature/) [![Twilio logo](https://webhookrelay.com/verify-zendesk-webhook-signature/images/landing/logos/twilio.svg) Twilio](https://webhookrelay.com/verify-zendesk-webhook-signature/verify-twilio-webhook-signature/) [**S** Standard Webhooks](https://webhookrelay.com/verify-zendesk-webhook-signature/verify-standard-webhooks-signature/) [![Square logo](https://webhookrelay.com/verify-zendesk-webhook-signature/images/landing/logos/square.svg) Square](https://webhookrelay.com/verify-zendesk-webhook-signature/verify-square-webhook-signature/) [**L** LINE](https://webhookrelay.com/verify-zendesk-webhook-signature/verify-line-webhook-signature/) [**A** Airwallex](https://webhookrelay.com/verify-zendesk-webhook-signature/verify-airwallex-webhook-signature/) [**M** MyFatoorah](https://webhookrelay.com/verify-zendesk-webhook-signature/verify-myfatoorah-webhook-signature/) [![DocuSign logo](https://webhookrelay.com/verify-zendesk-webhook-signature/images/landing/logos/docusign.svg) DocuSign](https://webhookrelay.com/verify-zendesk-webhook-signature/verify-docusign-webhook-signature/) [**K** Klarna](https://webhookrelay.com/verify-zendesk-webhook-signature/verify-klarna-webhook-signature/) [**A** Attio](https://webhookrelay.com/verify-zendesk-webhook-signature/verify-attio-webhook-signature/) [HMAC Generator](https://webhookrelay.com/verify-zendesk-webhook-signature/hmac-verification/) [CloudEvents Validator](https://webhookrelay.com/verify-zendesk-webhook-signature/cloudevents-validator/) [Webhook Tester (Bin)](https://webhookrelay.com/verify-zendesk-webhook-signature/webhook-bin/) Receiving Zendesk webhooks on a server behind a firewall or on localhost? Webhook Relay can [forward them to your internal service](https://webhookrelay.com/verify-zendesk-webhook-signature/webhooks/) and even [verify or transform them](https://webhookrelay.com/verify-zendesk-webhook-signature/features/transform-webhooks/) before delivery. --- --- title: Receiving webhooks on localhost | WebhookRelay meta: "og: title": "Receiving webhooks on localhost" description: Receive webhooks on localhost or private networks with Webhook Relay forward command url: https://webhookrelay.com/docs/webhooks/internal/localhost.md file: /docs/webhooks/internal/localhost.md --- ![Stripes](https://webhookrelay.com/docs/webhooks/internal/localhost/images/stripes.svg) Documentation **Fundamentals** # **Receiving webhooks on localhost** Receive webhooks on localhost or private networks with Webhook Relay forward command While developing and testing 3rd party integrations, it is useful to have an ability to receive webhooks on your localhost. To start with, navigate to the home page helper for [internal destinations](https://my.webhookrelay.com/new-internal-destination): ![Forward to internal URL](https://webhookrelay.com/docs/webhooks/internal/localhost/images/docs/webhooks/internal_forward.png) Follow instructions to setup the agent. Once the agent is running, webhooks will be sent to your private URL. ## [Using it again](#using-it-again) You don't have to setup the bucket and forwarding again. Once the configuration is done, you can call the `forward` command at any time: ``` relay forward --bucket YOUR-BUCKET-NAME ``` Did this page help you? --- --- title: AWS SQS | WebhookRelay meta: "og: title": "AWS SQS" description: Poll messages from Amazon SQS queues and send webhook data to SQS using Webhook Relay service connections. url: https://webhookrelay.com/docs/service-connections/aws_sqs.md file: /docs/service-connections/aws_sqs.md --- ![Stripes](https://webhookrelay.com/docs/service-connections/aws_sqs/images/stripes.svg) Documentation **Fundamentals** # **AWS SQS** Poll messages from Amazon SQS queues and send webhook data to SQS using Webhook Relay service connections. Connect Webhook Relay to **Amazon SQS** to poll messages from queues (input) or send webhook data as SQS messages (output). ## [Prerequisites](#prerequisites) - An [AWS service connection](https://webhookrelay.com/docs/service-connections/aws_sqs/docs/service-connections/) with credentials that have SQS permissions - An SQS queue in your AWS account ### [IAM Permissions](#iam-permissions) **For SQS Input (poll messages):** - `sqs:ReceiveMessage` - `sqs:DeleteMessage` - `sqs:GetQueueAttributes` **For SQS Output (send messages):** - `sqs:SendMessage` ## [SQS Input — Poll Messages from a Queue](#sqs-input-poll-messages-from-a-queue) SQS inputs use long-polling (20-second wait, up to 10 messages per poll) to continuously receive messages from your queue. Messages are deleted from the queue after successful relay into your bucket. ### [Configuration](#configuration) | Field | Required | Description | | --- | :---: | --- | | `queue_url` | Yes | Full SQS queue URL (e.g. `https://sqs.us-east-1.amazonaws.com/123456789/my-queue`) | | `region` | No | AWS region — auto-extracted from the queue URL if not provided | Once added, you can test it by using "Send and receive messages" button: ![SQS Input](https://webhookrelay.com/docs/service-connections/aws_sqs/images/docs/sc/sc_send_test_message.png) ## [SQS Output — Send Webhooks to a Queue](#sqs-output-send-webhooks-to-a-queue) SQS outputs forward incoming webhook data as messages to your SQS queue. Every webhook that arrives in the bucket is sent as an SQS message. ### [Configuration](#configuration-1) | Field | Required | Description | | --- | :---: | --- | | `queue_url` | Yes | Full SQS queue URL | | `region` | No | AWS region — auto-extracted from the URL | ## [Example: Bridge GCP Pub/Sub to AWS SQS](#example-bridge-gcp-pubsub-to-aws-sqs) Route messages from a GCP Pub/Sub subscription into an AWS SQS queue. This is ideal for cross-cloud event processing where your consumers read from SQS: 1. Create a [GCP service connection](https://webhookrelay.com/docs/service-connections/aws_sqs/docs/service-connections/) with Pub/Sub subscriber permissions 2. Create an [AWS service connection](https://webhookrelay.com/docs/service-connections/aws_sqs/docs/service-connections/) with SQS send permissions 3. Create a bucket in Webhook Relay 4. Add a **GCP Pub/Sub input** on the bucket 5. Add an **AWS SQS output** on the bucket Messages published to the Pub/Sub topic are automatically delivered to your SQS queue. ### [Transform Between Formats](#transform-between-formats) GCP Pub/Sub and AWS SQS use different message formats. Attach a [Function](https://webhookrelay.com/docs/service-connections/aws_sqs/docs/webhooks/functions/) to the bucket to reshape the payload. For example, convert a Pub/Sub message into a structured format for your SQS consumers: ``` const pubsubMessage = JSON.parse(r.body) // Restructure for your SQS consumer const sqsPayload = { source: "gcp-pubsub", topic: pubsubMessage.subscription, data: pubsubMessage.data, attributes: pubsubMessage.attributes, received_at: new Date().toISOString() } r.setBody(JSON.stringify(sqsPayload)) ``` See the [JSON encoding](https://webhookrelay.com/docs/service-connections/aws_sqs/docs/webhooks/functions/manipulating-json/) guide for more payload transformation examples. ## [Example: SQS to HTTPS Webhook Delivery](#example-sqs-to-https-webhook-delivery) Poll messages from an SQS queue and forward them to any HTTPS endpoint — useful when you want to process SQS messages with a service that only accepts webhooks: 1. Create an AWS service connection 2. Create a bucket with an **SQS input** pointing to your queue 3. Add a [public destination](https://webhookrelay.com/docs/service-connections/aws_sqs/docs/webhooks/public/public-destination/) as an output You can also forward to [localhost](https://webhookrelay.com/docs/service-connections/aws_sqs/docs/webhooks/internal/localhost/) for local development, letting you consume SQS messages on your machine without deploying to AWS. ## [Example: Fan-Out from SQS to Multiple Destinations](#example-fan-out-from-sqs-to-multiple-destinations) A single SQS input can feed multiple outputs on the same bucket. For example, forward SQS messages simultaneously to: - A GCP Pub/Sub topic (cross-cloud replication) - An [S3 bucket](https://webhookrelay.com/docs/service-connections/aws_sqs/docs/service-connections/aws_s3/) (archival) - An HTTPS API (real-time processing) This lets you fan out messages without configuring multiple SQS consumers or SNS subscriptions. Did this page help you? --- --- title: Azure | WebhookRelay meta: "og: title": Azure description: Azure service connections for Webhook Relay are in progress — Cosmos DB, Blob Storage, Service Bus and more. Contact us to enable early access for your account. url: https://webhookrelay.com/docs/service-connections/azure.md file: /docs/service-connections/azure.md --- ![Stripes](https://webhookrelay.com/docs/service-connections/azure/images/stripes.svg) Documentation **Fundamentals** # **Azure** Azure service connections for Webhook Relay are in progress — Cosmos DB, Blob Storage, Service Bus and more. Contact us to enable early access for your account. Webhook Relay service connections for **Microsoft Azure** are **in progress**. We already support [AWS](https://webhookrelay.com/docs/service-connections/azure/docs/service-connections/) (S3, SQS, SNS) and [GCP](https://webhookrelay.com/docs/service-connections/azure/docs/service-connections/gcp_pubsub/) (Pub/Sub, Cloud Storage), and Azure is next on the roadmap. ## [What's coming](#whats-coming) The same input/output model as our other [service connections](https://webhookrelay.com/docs/service-connections/azure/docs/service-connections/) — relay events from Azure into your buckets (inputs), and forward incoming webhooks straight into Azure services (outputs): - **Azure Cosmos DB** — write webhook events as documents - **Azure Blob Storage** — store payloads as objects and emit notifications - **Azure Service Bus / Event Grid** — publish to and subscribe from messages ## [Want it enabled for your account?](#want-it-enabled-for-your-account) If you'd like to start ingesting into **Cosmos DB**, **Azure Blob Storage**, Service Bus or another Azure service, we can turn it on for you early. [Contact us](https://webhookrelay.com/docs/service-connections/azure/contact/) or email **[info@webhookrelay.com](https://webhookrelay.com/docs/service-connections/azure//mailto:info@webhookrelay.com)** with the Azure services you need, and we'll enable the feature for your account. ## [Use Azure today](#use-azure-today) While native service connections land, you can already reach Azure with Webhook Relay: - Deliver webhooks to **any Azure HTTPS endpoint** (Functions, App Service, API Management) with a [public destination](https://webhookrelay.com/docs/service-connections/azure/docs/webhooks/public/public-destination/) - Forward events into a **private Azure VNet** through the [Webhook Relay agent](https://webhookrelay.com/docs/service-connections/azure/docs/installation/cli/) — no public ingress required Did this page help you? --- --- title: GCP Pub/Sub | WebhookRelay meta: "og: title": "GCP Pub/Sub" description: Subscribe to Google Cloud Pub/Sub topics and publish webhook data to Pub/Sub using Webhook Relay service connections. url: https://webhookrelay.com/docs/service-connections/gcp_pubsub.md file: /docs/service-connections/gcp_pubsub.md --- ![Stripes](https://webhookrelay.com/docs/service-connections/gcp_pubsub/images/stripes.svg) Documentation **Fundamentals** # **GCP Pub/Sub** Subscribe to Google Cloud Pub/Sub topics and publish webhook data to Pub/Sub using Webhook Relay service connections. Connect Webhook Relay to **Google Cloud Pub/Sub** to receive messages from subscriptions (input) or publish webhook data to topics (output). ## [Prerequisites](#prerequisites) - A [GCP service connection](https://webhookrelay.com/docs/service-connections/gcp_pubsub/docs/service-connections/) with a service account that has Pub/Sub permissions - A Pub/Sub topic and subscription in your GCP project ### [GCP Roles](#gcp-roles) **For Pub/Sub Input (subscribe):** - `roles/pubsub.subscriber` — the subscription must already exist in your project **For Pub/Sub Output (publish):** - `roles/pubsub.publisher` ## [Pub/Sub Input — Receive Messages from a Subscription](#pubsub-input-receive-messages-from-a-subscription) Pub/Sub inputs subscribe to an existing subscription and relay messages into your Webhook Relay bucket. Messages are auto-acknowledged after successful relay. Message data and attributes are wrapped in a JSON envelope. ### [Configuration](#configuration) | Field | Required | Description | | --- | :---: | --- | | `subscription_name` | Yes | Pub/Sub subscription name (must already exist in the project) | > The subscription must be pre-created in your GCP project before adding it as an input. Webhook Relay does not create subscriptions automatically. ## [Pub/Sub Output — Publish Webhooks to a Topic](#pubsub-output-publish-webhooks-to-a-topic) ![GCP Pub/Sub output](https://webhookrelay.com/docs/service-connections/gcp_pubsub/images/docs/sc/add_gcp_pubsub.png) Pub/Sub outputs publish incoming webhook data as messages to your Pub/Sub topic. The topic must already exist in your project. You can find them in your "Pub/Sub" section in the GCP console: ![Browse your GCP PubSub topics](https://webhookrelay.com/docs/service-connections/gcp_pubsub/images/docs/sc/sc_gcs_sub.png) ### [Configuration](#configuration-1) | Field | Required | Description | | --- | :---: | --- | | `topic_name` | Yes | Pub/Sub topic name | ## [Example: Bridge AWS SQS to GCP Pub/Sub](#example-bridge-aws-sqs-to-gcp-pubsub) Route messages from an AWS SQS queue into a GCP Pub/Sub topic. This is ideal for migrating workloads across cloud providers or running multi-cloud architectures: 1. Create an [AWS service connection](https://webhookrelay.com/docs/service-connections/gcp_pubsub/docs/service-connections/) with SQS read permissions 2. Create a [GCP service connection](https://webhookrelay.com/docs/service-connections/gcp_pubsub/docs/service-connections/) with Pub/Sub publisher permissions 3. Create a bucket in Webhook Relay 4. Add an **[AWS SQS](https://webhookrelay.com/docs/service-connections/gcp_pubsub/docs/service-connections/aws_sqs/) input** on the bucket 5. Add a **GCP Pub/Sub output** on the bucket Messages polled from SQS are automatically published to your Pub/Sub topic. ### [Transform Between Formats](#transform-between-formats) Attach a [Function](https://webhookrelay.com/docs/service-connections/gcp_pubsub/docs/webhooks/functions/) to adapt the message format. For example, convert an SQS message into a structured Pub/Sub payload: ``` const sqsMessage = JSON.parse(r.body) // Restructure for Pub/Sub consumers const pubsubPayload = { source: "aws-sqs", original_message_id: sqsMessage.MessageId, data: sqsMessage.Body, bridged_at: new Date().toISOString() } r.setBody(JSON.stringify(pubsubPayload)) ``` See the [JSON encoding](https://webhookrelay.com/docs/service-connections/gcp_pubsub/docs/webhooks/functions/manipulating-json/) guide for more transformation examples. ## [Example: Pub/Sub to Any HTTPS Endpoint](#example-pubsub-to-any-https-endpoint) Deliver Pub/Sub messages as webhooks to any API that accepts HTTPS requests. This works well for services without native GCP integration: 1. Create a GCP service connection 2. Create a bucket with a **Pub/Sub input** 3. Add a [public destination](https://webhookrelay.com/docs/service-connections/gcp_pubsub/docs/webhooks/public/public-destination/) (any HTTPS URL) Use a [Function](https://webhookrelay.com/docs/service-connections/gcp_pubsub/docs/webhooks/functions/) to add authentication or transform the payload before delivery: ``` const message = JSON.parse(r.body) // Forward only the message data, add auth header r.setBody(message.data) r.setHeader("Authorization", "Bearer " + cfg.get("API_TOKEN")) r.setHeader("Content-Type", "application/json") ``` See [configuration variables](https://webhookrelay.com/docs/service-connections/gcp_pubsub/docs/webhooks/functions/modify-request/#getting-configuration-values) for how to securely store API tokens in functions. ## [Example: Pub/Sub to Localhost for Development](#example-pubsub-to-localhost-for-development) Receive Pub/Sub messages on your local machine during development — no need to deploy to GCP: 1. Create a GCP service connection 2. Create a bucket with a **Pub/Sub input** 3. Add an [internal destination](https://webhookrelay.com/docs/service-connections/gcp_pubsub/docs/webhooks/internal/localhost/) pointing to `http://localhost:3000/webhook` 4. Run the [Webhook Relay agent](https://webhookrelay.com/docs/service-connections/gcp_pubsub/docs/installation/cli/) locally Messages from your Pub/Sub subscription are forwarded to your local server in real time. ## [Example: AWS SNS to GCP Pub/Sub](#example-aws-sns-to-gcp-pubsub) Publish [AWS SNS](https://webhookrelay.com/docs/service-connections/gcp_pubsub/docs/service-connections/aws_sns/) messages to a Pub/Sub topic. Useful when your processing pipeline runs on GCP but events originate in AWS: 1. Create an [AWS service connection](https://webhookrelay.com/docs/service-connections/gcp_pubsub/docs/service-connections/) with SNS subscribe permissions 2. Create a [GCP service connection](https://webhookrelay.com/docs/service-connections/gcp_pubsub/docs/service-connections/) with Pub/Sub publisher permissions 3. Create a bucket with an **AWS SNS input** and a **GCP Pub/Sub output** Optionally add a function to filter or transform the SNS message before publishing to Pub/Sub. Did this page help you? --- --- title: JSON encoding | WebhookRelay meta: "og: title": "JSON encoding" description: How to encode and decode JSON in Webhook Relay Functions url: https://webhookrelay.com/docs/webhooks/functions/manipulating-json.md file: /docs/webhooks/functions/manipulating-json.md --- ![Stripes](https://webhookrelay.com/docs/webhooks/functions/manipulating-json/images/stripes.svg) Documentation **Fundamentals** # **JSON encoding** How to encode and decode JSON in Webhook Relay Functions JSON is a popular format in which services exchange information. Functions allow parsing and modifying these payloads to integrate different services with each other. Some of the examples that you can do: - Decode Stripe webhook and encode it into a Slack or Discord notification - Change Mailgun delivery notification into a Discord message - Send an email when a change is pushed to a specific Bitbucket branch ## [Decode JSON](#decode-json) To decode JSON in a function: ``` // example payload: // { // "user": "Peter", // "age": 25, // "city": "Edinburgh" // } const requestPayload = JSON.parse(r.body) // now, requestPayload is a normal JSON object and we // can access individual values r.setBody(requestPayload.user) // request will now have a single value 'Peter' in the body ``` ``` -- import "json" package when working with JSON local json = require("json") -- example payload: -- { -- "user": "Peter", -- "age": 25, -- "city": "Edinburgh" -- } local request_payload, err = json.decode(r.RequestBody) if err then error(err) end -- now, request_payload is a normal JSON object and we -- can access individual values r:SetRequestBody(request_payload.user) -- request will now have a single value 'Peter' in the body ``` ## [Encode to JSON](#encode-to-json) To encode a structure into a JSON string: ``` // constructing a new object that we will encode // into a JSON string const newPayload = { action: "hello", message: "world" } // encoding const encodedPayload = JSON.stringify(newPayload) r.setBody(encodedPayload) // webhook request body is now changed to: // { // "action": "hello", // "message": "world" // } ``` ``` -- import "json" package when working with JSON local json = require("json") -- constructing a new object that we will encode -- into a JSON string local new_payload = { action= "hello", message= "world"} -- encoding local encoded_payload, err = json.encode(new_payload) if err then error(err) end r:SetRequestBody(encoded_payload) -- webhook request body is now changed to: -- { -- "action": "hello", -- "message: "world" -- } ``` Did this page help you? --- --- title: Alerting from functions | WebhookRelay meta: "og: title": "Alerting from functions" description: Inspect webhook requests and delivery responses in Webhook Relay functions, and POST an alert to any URL when something looks wrong. url: https://webhookrelay.com/docs/webhooks/functions/alerting.md file: /docs/webhooks/functions/alerting.md --- ![Stripes](https://webhookrelay.com/docs/webhooks/functions/alerting/images/stripes.svg) Documentation **Fundamentals** # **Alerting from functions** Inspect webhook requests and delivery responses in Webhook Relay functions, and POST an alert to any URL when something looks wrong. Functions can do more than reshape payloads — they can **inspect** the request or the delivery response and **alert** an external system when something is off. Two common patterns: 1. **Request function** (runs _before_ delivery) — parse the body; if it is not valid JSON (or a required field is missing), POST an alert to a URL. 2. **Response function** (runs _after_ delivery) — if the destination returned an empty body, POST an alert to a URL. Set `alert_webhook_url` as a [config variable](https://webhookrelay.com/docs/webhooks/functions/alerting/docs/webhooks/functions/modify-request/#getting-configuration-values) on the function so the alert endpoint is not hard-coded in the source. > For built-in delivery-failure incidents (policies, Slack/email destinations, auto-resolve), see [Alerts](https://webhookrelay.com/docs/webhooks/functions/alerting/docs/webhooks/alerts/). For a deeper look at post-delivery hooks, status codes, and rewriting the recorded response, see [Response (post-delivery) functions](https://webhookrelay.com/docs/webhooks/functions/alerting/docs/webhooks/functions/response-functions/). ## [Example 1: alert when the request is not valid JSON](#example-1-alert-when-the-request-is-not-valid-json) Attach this as a **request** function (before delivery). It tries to parse `r.body` as JSON. If parsing fails, it posts an alert to your URL and [stops forwarding](https://webhookrelay.com/docs/webhooks/functions/alerting/docs/webhooks/functions/modify-request/#filtering-requests) so a bad payload never reaches the destination. ``` // Request function — runs BEFORE delivery. // Alert if the body is missing or is not valid JSON. var url = cfg.get("alert_webhook_url"); if (!r.body || r.body.trim() === "") { if (url) { http.post(url, JSON.stringify({ type: "invalid_request", reason: "empty body", output: r.metadata["output_name"], path: r.path }), { headers: { "Content-Type": "application/json" } }); } r.stopForwarding(); return; } var payload; try { payload = JSON.parse(r.body); } catch (e) { if (url) { http.post(url, JSON.stringify({ type: "invalid_request", reason: "body is not valid JSON", error: String(e), output: r.metadata["output_name"], path: r.path }), { headers: { "Content-Type": "application/json" } }); } r.stopForwarding(); return; } // Optional: also require a field if (!payload || payload.id == null) { if (url) { http.post(url, JSON.stringify({ type: "invalid_request", reason: "missing required field: id", output: r.metadata["output_name"] }), { headers: { "Content-Type": "application/json" } }); } r.stopForwarding(); return; } // Body looks fine — continue delivery as-is (or call r.setBody(...) to reshape it). ``` ``` -- Request function — runs BEFORE delivery. -- Alert if the body is missing or is not valid JSON. local http = require("http") local json = require("json") local url = cfg:GetValue("alert_webhook_url") local function alert(reason, extra) if url == "" then return end local body = { type = "invalid_request", reason = reason, output = r.metadata["output_name"] or "", path = r.RequestPath or "" } if extra then for k, v in pairs(extra) do body[k] = v end end local payload, err = json.encode(body) if err then error(err) end http.request("POST", url, { headers = { ["Content-Type"] = "application/json" }, body = payload }) end if r.RequestBody == nil or r.RequestBody == "" then alert("empty body") r:StopForwarding() return end local payload, err = json.decode(r.RequestBody) if err then alert("body is not valid JSON", { error = tostring(err) }) r:StopForwarding() return end if payload == nil or payload.id == nil then alert("missing required field: id") r:StopForwarding() return end -- Body looks fine — continue delivery as-is -- (or call r:SetRequestBody(...) to reshape it). ``` ## [Example 2: alert when the response body is empty](#example-2-alert-when-the-response-body-is-empty) Attach this as a **response** function (after delivery). When the destination answers with a success status but an **empty** body — a common silent-failure pattern — post an alert to your URL. ``` // Response function — runs AFTER delivery. // Alert if the destination returned an empty body. var empty = !r.responseBody || r.responseBody.trim() === ""; if (!empty) { return; // body present — nothing to do } var url = cfg.get("alert_webhook_url"); if (!url) { return; } http.post(url, JSON.stringify({ type: "empty_response", reason: "destination returned an empty body", status: r.responseStatus, output: r.metadata["output_name"], destination: r.metadata["output_url"] }), { headers: { "Content-Type": "application/json" } }); ``` ``` -- Response function — runs AFTER delivery. -- Alert if the destination returned an empty body. local http = require("http") local json = require("json") local empty = r.ResponseBody == nil or r.ResponseBody == "" if not empty then return -- body present — nothing to do end local url = cfg:GetValue("alert_webhook_url") if url == "" then return end local payload, err = json.encode({ type = "empty_response", reason = "destination returned an empty body", status = r.ResponseStatus, output = r.metadata["output_name"] or "", destination = r.metadata["output_url"] or "" }) if err then error(err) end http.request("POST", url, { headers = { ["Content-Type"] = "application/json" }, body = payload }) ``` You can combine this with a status check (`r.responseStatus === 0` or `>= 400`) if you also want alerts on timeouts and HTTP errors. See [Response (post-delivery) functions](https://webhookrelay.com/docs/webhooks/functions/alerting/docs/webhooks/functions/response-functions/) for failure-status examples and for rewriting the recorded response when a `200` should count as failed. ## [Setup checklist](#setup-checklist) 1. Create a function in the dashboard (use the **REQUEST** tab for example 1, **RESPONSE** tab for example 2). 2. Add a config variable `alert_webhook_url` pointing at your Slack incoming webhook, PagerDuty, Discord, or any HTTP endpoint that accepts POST JSON. 3. Attach the function to an output: - Request function → `function_id` (before delivery) - Response function → `response_function_id` (after delivery) ## [Related](#related) - [Read, write request data](https://webhookrelay.com/docs/webhooks/functions/alerting/docs/webhooks/functions/modify-request/) — `r.body`, `r.setBody`, `r.stopForwarding`, config variables. - [Make HTTP requests](https://webhookrelay.com/docs/webhooks/functions/alerting/docs/webhooks/functions/make-http-request/) — `http.post` / `http.request` used to send the alert. - [Response (post-delivery) functions](https://webhookrelay.com/docs/webhooks/functions/alerting/docs/webhooks/functions/response-functions/) — full response API, empty-200 handling, flagging the recorded response. - [Accessing metadata](https://webhookrelay.com/docs/webhooks/functions/alerting/docs/webhooks/functions/accessing-metadata/) — `output_name`, `output_url`, and other fields useful in alerts. Did this page help you? --- --- title: Outbound throttling: control webhook delivery speed meta: "og: title": "Outbound throttling: control webhook delivery speed" description: Pace outgoing webhooks to a steady rate or fixed concurrency. Outbound throttling protects fragile endpoints and respects downstream rate limits. url: https://webhookrelay.com/docs/webhooks/outbound-throttling.md file: /docs/webhooks/outbound-throttling.md --- ![Stripes](https://webhookrelay.com/docs/webhooks/outbound-throttling/images/stripes.svg) Documentation **Fundamentals** # **Outbound throttling: control webhook delivery speed** Pace outgoing webhooks to a steady rate or fixed concurrency. Outbound throttling protects fragile endpoints and respects downstream rate limits. **Outbound throttling** puts a governor between Webhook Relay and your destination. Instead of forwarding the instant an event arrives, Webhook Relay holds each webhook and releases it at the pace you set — turning a jagged spike into a smooth, predictable stream your server can handle. Nothing is dropped to keep the pace; throttled webhooks wait their turn, oldest first. Throttling is available on **every plan**. For the product overview, see the [throttling feature page](https://webhookrelay.com/docs/webhooks/outbound-throttling/features/throttling/). ## [Why throttle outgoing webhooks](#why-throttle-outgoing-webhooks) A quiet integration can turn into a flood without warning — a provider replays a backlog, a sale goes live, or a batch job fires thousands of events at once. Webhook Relay absorbs that spike easily, but your destination might not. The usual symptoms are timeouts, `429 Too Many Requests` responses, a database pegged at 100%, and failures that cascade past the webhook endpoint. Throttling solves this without you having to stand up a queue, add workers, and write your own backpressure logic. ## [Two ways to set the pace](#two-ways-to-set-the-pace) | Mode | What it does | Use it when | | --- | --- | --- | | **Rate** | Deliver at most _N_ webhooks per second, minute or hour. | A downstream API allows 100 calls/minute, or an endpoint copes up to 20 req/s. | | **Concurrency** | Cap how many deliveries are in flight at once. | You want strictly one-at-a-time delivery (set it to 1), or a fixed amount of parallelism. | You can use either on its own or both together — for example "no more than 50/second **and** at most 5 in flight". Webhook Relay never exceeds whichever limit is tighter. ## [Turn on throttling](#turn-on-throttling) Throttling is configured **per output destination**, so each destination gets its own budget and a slow consumer can't starve the rest. 1. Open your bucket and select the **output destination** you want to pace. 2. In **Delivery controls**, open **Throttling** and switch it on. 3. Set a **Rate** (e.g. 100 per minute) and/or a **Concurrency** limit (e.g. 5). 4. Optionally set a **queue limit** to enable backpressure (see below), then **Save**. New webhooks to that destination are now released at the pace you chose. Events waiting for their turn appear in the request log in order and go out the moment there's room. ## [Backpressure with a queue limit](#backpressure-with-a-queue-limit) By default, throttled webhooks simply wait in line. Add an optional **queue limit** and Webhook Relay pushes back at the source instead of building an endless backlog: once a destination's queue is full it returns a standard `429 Too Many Requests`, so a runaway sender slows down. Anything still waiting past its deadline is cleaned up automatically, so queues never rot. ## [Works for public and internal destinations](#works-for-public-and-internal-destinations) Throttling behaves the same for: - **Public HTTPS endpoints** — your API, a partner's API, any SaaS webhook URL. - **[Internal destinations](https://webhookrelay.com/docs/webhooks/outbound-throttling/features/webhook-to-internal-server/)** — services behind your firewall or on localhost reached through the Webhook Relay agent. This is especially useful when the thing you're protecting is a small service on your own infrastructure. ## [Better together with durable retries](#better-together-with-durable-retries) Throttling decides _how fast_; [durable retries](https://webhookrelay.com/docs/webhooks/outbound-throttling/docs/webhooks/durable-webhooks/) decide _how long_. Turn on both and retries to a recovering endpoint respect the same speed limit as fresh traffic — so you never accidentally finish off a server that's only just getting back on its feet. ## [Frequently asked questions](#frequently-asked-questions) ### [Does throttling drop webhooks?](#does-throttling-drop-webhooks) No. Throttling only changes _when_ a webhook is delivered, not whether. Events wait their turn in order and go out as soon as there's capacity. (If you also set a queue limit, senders get a `429` once the queue is full, rather than events being silently discarded.) ### [What's the difference between rate and concurrency?](#whats-the-difference-between-rate-and-concurrency) Rate caps how many webhooks leave per unit of time (e.g. 100/minute). Concurrency caps how many deliveries are in flight simultaneously (e.g. 5 at once). Rate controls throughput; concurrency controls parallel load. Use whichever matches the limit your destination cares about — or both. ### [Can I match a downstream API's rate limit exactly?](#can-i-match-a-downstream-apis-rate-limit-exactly) Yes — set the rate to the same budget the downstream API allows (for example 100 per minute) and Webhook Relay will never exceed it, so you stop getting rate-limited. ### [Is throttling available on the free plan?](#is-throttling-available-on-the-free-plan) Yes. Outbound throttling is included on every plan. ## [Related](#related) - [Throttling — feature overview](https://webhookrelay.com/docs/webhooks/outbound-throttling/features/throttling/) - [Durable webhooks — reliable delivery with automatic retries](https://webhookrelay.com/docs/webhooks/outbound-throttling/docs/webhooks/durable-webhooks/) - [Forwarding to internal services](https://webhookrelay.com/docs/webhooks/outbound-throttling/features/webhook-to-internal-server/) Did this page help you? --- --- title: Fix TLS/SSL Handshake Errors | WebhookRelay meta: "og: title": "Fix TLS/SSL Handshake Errors" description: What each common TLS/SSL handshake error means — handshake failure, protocol version, unsupported protocol, certificate verify failed — and how to fix it. url: https://webhookrelay.com/docs/webhooks/tls-ssl-errors.md file: /docs/webhooks/tls-ssl-errors.md --- ![Stripes](https://webhookrelay.com/docs/webhooks/tls-ssl-errors/images/stripes.svg) Documentation **Fundamentals** # **Fix TLS/SSL handshake errors: unsupported protocol, wrong version number, handshake failure** What each common TLS/SSL handshake error means — handshake failure, protocol version, unsupported protocol, certificate verify failed — and how to fix it. Almost every TLS/SSL handshake error means the same thing underneath: **the client and the server could not agree on a protocol version or cipher suite**. One side offered only modern TLS 1.2/1.3; the other offered only legacy TLS 1.0/1.1 (or a weak cipher) — and there was no overlap, so the handshake was aborted. This happens constantly when webhooks cross a boundary between _modern_ and _legacy_ infrastructure: a new SaaS sender that speaks only TLS 1.3 trying to reach an old on-prem appliance, or a hardened endpoint that has disabled TLS 1.0/1.1 rejecting an older client that can't go higher. This page is a reference for the **exact error strings** different tools print, what each one means, and how to fix it — including how [Webhook Relay's TLS compatibility](#how-webhook-relay-bridges-the-gap) lets a legacy sender deliver webhooks that a modern endpoint would refuse, and lets you deliver to destinations with non-standard certificates. ## [Quick diagnosis](#quick-diagnosis) | What you see | What it usually means | | --- | --- | | `unsupported protocol` / `protocol_version` / `ERR_SSL_VERSION_OR_CIPHER_MISMATCH` | One side requires a TLS version the other has disabled (e.g. server only allows TLS 1.2+, client only offers TLS 1.0/1.1, or vice-versa). | | `sslv3 alert handshake failure` / `handshake_failure` | No shared cipher suite, a missing client certificate, or a rejected protocol version. | | `wrong version number` | One side is speaking plain HTTP to an HTTPS port (or TLS to a plaintext port) — often not a version problem at all. | | `dh key too small` / `no cipher overlap` | The server's certificate or DH parameters use a cipher the modern client refuses. | | `certificate verify failed` / `self-signed certificate` / `unable to verify the first certificate` | The version is fine, but the endpoint's certificate isn't trusted by a public CA (self-signed, internal CA, or incomplete chain). | ## [`error:0A000102:SSL routines::unsupported protocol`](#error0a000102ssl-routinesunsupported-protocol) The OpenSSL 3.x "no common protocol version" error. You'll also see it wrapped by curl and Node.js: ``` curl: (35) error:0A000102:SSL routines::unsupported protocol # OpenSSL 1.1.x phrased it as: error:1425F102:SSL routines:ssl_choose_client_version:unsupported protocol # Node.js: Error: write EPROTO ... error:0A000102:SSL routines::unsupported protocol ``` **What it means:** the client and server share no enabled TLS protocol version. The most common cause today is a modern client (OpenSSL 3, which disables TLS 1.0/1.1 by default) connecting to a server that only speaks those old versions. **Fix on your side:** upgrade the server to TLS 1.2/1.3, or — if you genuinely must talk to a legacy box — explicitly re-enable an older protocol on the client (`curl --tlsv1.0`, or an OpenSSL config with `MinProtocol = TLSv1`). If the legacy system is the one _sending_ you webhooks, point it at a [Webhook Relay input with legacy TLS enabled](#how-webhook-relay-bridges-the-gap) instead of lowering TLS on everything else. ## [`error:14094410:SSL routines:ssl3_read_bytes:sslv3 alert handshake failure`](#error14094410ssl-routinesssl3_read_bytessslv3-alert-handshake-failure) Despite the `sslv3` label, this rarely involves SSLv3 — it's a generic **handshake failure alert (alert number 40)** from the peer. ``` # OpenSSL / curl curl: (35) error:14094410:SSL routines:ssl3_read_bytes:sslv3 alert handshake failure # Python ssl.SSLError: [SSL: SSLV3_ALERT_HANDSHAKE_FAILURE] sslv3 alert handshake failure (_ssl.c:1007) # OpenSSL 3.x error:0A000410:SSL routines::sslv3 alert handshake failure ``` **What it means:** the server rejected the handshake. Common causes are no shared cipher suite, the server requiring a client certificate you didn't present, or the server refusing the protocol version the client offered. **Fix on your side:** confirm the client and server share at least one cipher suite and TLS version, and supply a client certificate if the endpoint requires mutual TLS. ## [`error:1409442E:SSL routines:ssl3_read_bytes:tlsv1 alert protocol version`](#error1409442essl-routinesssl3_read_bytestlsv1-alert-protocol-version) The peer sent a **protocol_version alert (alert number 70)** — it explicitly rejected the TLS version you offered. ``` curl: (35) error:1409442E:SSL routines:ssl3_read_bytes:tlsv1 alert protocol version ``` **What it means:** you offered a TLS version the other side has disabled. Typically an older client (TLS 1.0/1.1 only) hitting a server that now requires TLS 1.2+. **Fix on your side:** upgrade the client's TLS library, or route the request through something that can negotiate the version the server expects. ## [`error:1408F10B:SSL routines:ssl3_get_record:wrong version number`](#error1408f10bssl-routinesssl3_get_recordwrong-version-number) This one is a common red herring — it's usually **not** a TLS version mismatch. ``` curl: (35) error:1408F10B:SSL routines:ssl3_get_record:wrong version number # OpenSSL 3.x error:0A00010B:SSL routines::wrong version number ``` **What it means:** the bytes received don't look like a TLS record at all. Almost always one of: - You sent **`https://` to a port that's serving plain HTTP** (or vice-versa). - A proxy or load balancer terminated TLS and is forwarding cleartext. - The wrong port entirely. **Fix:** check the scheme and port before anything else — match `https://` to the TLS port and `http://` to the plaintext one. ## [`received fatal alert: protocol_version` (Java)](#received-fatal-alert-protocol_version-java) The Java/JSSE wording for the same protocol-version rejection above. ``` javax.net.ssl.SSLHandshakeException: Received fatal alert: protocol_version # A related JSSE message when the JVM has disabled the only protocol on offer: javax.net.ssl.SSLHandshakeException: No appropriate protocol (protocol is disabled or cipher suites are inappropriate) ``` **What it means:** the JVM and the peer have no enabled TLS version in common. Modern JDKs disable TLS 1.0/1.1 in `jdk.tls.disabledAlgorithms`, so a JVM talking to a legacy endpoint (or an old JVM talking to a hardened one) fails here. **Fix on your side:** align the protocol versions (`-Dhttps.protocols=TLSv1.2`), or update `java.security`. If a legacy Java service is _posting_ webhooks and can't reach a modern endpoint, give it a [Webhook Relay input that accepts its TLS version](#how-webhook-relay-bridges-the-gap) instead of relaxing JVM-wide security policy. ## [`\[SSL: NO_PROTOCOLS_AVAILABLE\] no protocols available` (Python)](#ssl-no_protocols_available-no-protocols-available-python) ``` ssl.SSLError: [SSL: NO_PROTOCOLS_AVAILABLE] no protocols available (_ssl.c:997) # Often surfaced through requests as: requests.exceptions.SSLError: HTTPSConnectionPool(host='...', port=443): Max retries exceeded ... [SSL: NO_PROTOCOLS_AVAILABLE] no protocols available ``` **What it means:** every protocol version Python's `ssl` module would offer has been disabled (commonly because the OpenSSL build disabled TLS 1.0/1.1 and the target supports nothing newer). **Fix on your side:** target an endpoint that supports TLS 1.2+, or — only when you control and trust the legacy endpoint — lower `ssl.SSLContext.minimum_version`. ## [`ERR_SSL_VERSION_OR_CIPHER_MISMATCH` (Chrome / Edge)](#err_ssl_version_or_cipher_mismatch-chrome-edge) The browser equivalent, and by far the most-searched of this family. ``` This site can't provide a secure connection example.com uses an unsupported protocol. ERR_SSL_VERSION_OR_CIPHER_MISMATCH # In the console / network log: net::ERR_SSL_VERSION_OR_CIPHER_MISMATCH # Firefox phrases it as: Secure Connection Failed — SSL_ERROR_UNSUPPORTED_VERSION SSL_ERROR_NO_CYPHER_OVERLAP ``` **What it means:** the browser (which has dropped TLS 1.0/1.1 and weak ciphers) and the server share no protocol version or cipher. The server is usually too old or misconfigured. **Fix on your side:** enable TLS 1.2/1.3 and a modern cipher suite on the server, and make sure the certificate isn't using a deprecated signature. ## [`Error 525: SSL handshake failed` (Cloudflare)](#error-525-ssl-handshake-failed-cloudflare) ``` Error 525: SSL handshake failed ``` **What it means:** Cloudflare (a modern TLS client) couldn't complete the handshake with your origin server — typically the origin requires an older protocol, presents an incomplete certificate chain, or isn't listening on 443. **Fix on your side:** ensure the origin supports the TLS version Cloudflare offers and serves a complete, valid certificate chain. ## [`remote error: tls: protocol version not supported` (Go)](#remote-error-tls-protocol-version-not-supported-go) ``` remote error: tls: protocol version not supported tls: server selected unsupported protocol version 301 ``` **What it means:** Go's `crypto/tls` sets `MinVersion` to TLS 1.2 by default, so it refuses an endpoint that offers only TLS 1.0 (version `301`) or 1.1. **Fix on your side:** only if you control the endpoint, set a lower `tls.Config{MinVersion: tls.VersionTLS10}` — but prefer upgrading the endpoint or bridging it. ## [`certificate verify failed` / `self-signed certificate` / `unable to verify the first certificate`](#certificate-verify-failed-self-signed-certificate-unable-to-verify-the-first-certificate) A different failure mode: the protocol version is fine, but the client can't **verify the endpoint's certificate** against a trusted CA. ``` # curl curl: (60) SSL certificate problem: self-signed certificate curl: (60) SSL certificate problem: unable to get local issuer certificate # Node.js Error: unable to verify the first certificate (UNABLE_TO_VERIFY_LEAF_SIGNATURE) Error: self-signed certificate in certificate chain # Python ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self signed certificate (_ssl.c:1007) # Go x509: certificate signed by unknown authority # Chrome NET::ERR_CERT_AUTHORITY_INVALID ``` **What it means:** the endpoint presents a certificate that isn't signed by a publicly trusted CA — a self-signed cert, a private/internal CA, an incomplete chain, or an expired certificate. **Fix on your side:** install a publicly trusted certificate, or send the full chain (including intermediates). When the destination is an internal or legacy box you control and trust, [Webhook Relay can skip verification for that one destination](#how-webhook-relay-bridges-the-gap). ## [How Webhook Relay bridges the gap](#how-webhook-relay-bridges-the-gap) Webhook Relay terminates TLS independently on the way **in** (the input that receives webhooks) and on the way **out** (delivery to your destination), so the two legs don't have to share the same TLS settings. The controls live in two places. ### [Accept legacy senders — TLS compatibility on the input](#accept-legacy-senders-tls-compatibility-on-the-input) The version and cipher errors above happen when a **legacy sender** can't complete a modern handshake. If that sender is delivering webhooks to you, you don't have to weaken anything else — just relax TLS on the **input** it posts to. Each input has a **TLS compatibility** setting with two controls: - **TLS version** — the _minimum_ version the input accepts. The default is **TLS 1.3**; lower it to **TLS 1.2** for senders that still require it (PayPal webhooks, for example), or further when you must. - **Legacy TLS compatibility (TLS 1.0 + wide ciphers)** — a toggle that makes the input accept TLS versions **down to 1.0 and a wider legacy cipher set**, for the oldest systems that would otherwise fail with `sslv3 alert handshake failure`, `unsupported protocol` or `ERR_SSL_VERSION_OR_CIPHER_MISMATCH`. ![Per-input TLS compatibility settings — a minimum TLS version dropdown and a "Legacy TLS compatibility (TLS 1.0 + wide ciphers)" toggle](https://webhookrelay.com/docs/webhooks/tls-ssl-errors/images/docs/webhooks/tls/tls_settings.png) Webhook Relay accepts the old handshake on that input and forwards the event onward over modern TLS — so one legacy sender no longer forces you to lower TLS across your whole stack. **Legacy settings apply ****per input domain**** and lower the security baseline, so enable them only on the inputs that genuinely need them. Setting a custom minimum ****TLS version**** is available on ****Business and Pro****; the ****Legacy TLS compatibility**** toggle (down to TLS 1.0 + wide ciphers) is available on ****Pro****. See the **[TLS compatibility feature](https://webhookrelay.com/docs/webhooks/tls-ssl-errors/features/tls-compatibility/)** and **[pricing](https://webhookrelay.com/docs/webhooks/tls-ssl-errors/pricing/)**.** ### [Deliver to non-standard certificates — TLS verification on the output](#deliver-to-non-standard-certificates-tls-verification-on-the-output) On the delivery side the relevant control is **TLS verification**, found per destination under **Delivery controls**. Leave it on for normal endpoints; switch it **off** to deliver to a destination whose certificate can't be verified against public CAs — a self-signed cert, an internal CA or a legacy box — instead of failing with `certificate verify failed` or `unable to verify the first certificate`. ![Per-output Delivery controls with a TLS verification toggle](https://webhookrelay.com/docs/webhooks/tls-ssl-errors/images/docs/webhooks/tls/tls_output_disable_verification.png) Only disable verification for destinations you control and trust, typically on a private or internal network. ## [Turn it on](#turn-it-on) **To accept a legacy sender (input):** 1. Open the **input** the sender delivers to and find **TLS compatibility**. 2. Set the **TLS version** to the lowest version that sender needs, and/or turn on **Legacy TLS compatibility (TLS 1.0 + wide ciphers)**. 3. **Save**, then have the sender retry — the handshake now succeeds and the webhook is relayed onward. **To deliver to a self-signed or internal certificate (output):** 1. Open the **output destination** and find **Delivery controls**. 2. Turn **TLS verification** off. 3. Re-send a webhook and confirm it's delivered in the request log. ## [Frequently asked questions](#frequently-asked-questions) ### [Why do I get "unsupported protocol" even though both sides support TLS?](#why-do-i-get-unsupported-protocol-even-though-both-sides-support-tls) They support TLS, but not the _same version_. Modern clients disable TLS 1.0/1.1; if the server only offers those, there is no version in common and the handshake fails with `unsupported protocol` or a `protocol_version` alert. One side has to meet the other — which is what a Webhook Relay input does when you lower its minimum TLS version or enable legacy TLS compatibility for a legacy sender. ### [Is "wrong version number" a TLS version problem?](#is-wrong-version-number-a-tls-version-problem) Usually not. `wrong version number` almost always means a scheme/port mismatch — `https://` pointed at a plaintext HTTP port (or a proxy terminating TLS early). Check the URL scheme and port first. ### [Is enabling legacy TLS 1.0/1.1 safe?](#is-enabling-legacy-tls-1011-safe) TLS 1.0 and 1.1 are deprecated and shouldn't be used on the public internet. Webhook Relay applies legacy TLS settings **per input domain**, so the safe pattern is to enable them **only on the one input a legacy sender needs** and leave every other input on the modern TLS 1.3 default. ### [Can I force a minimum TLS version for compliance?](#can-i-force-a-minimum-tls-version-for-compliance) Yes. Set a custom minimum **TLS version** on an input (Business or Pro) and Webhook Relay refuses handshakes below it. To deliver to an endpoint whose certificate can't be verified, use the per-output **TLS verification** toggle instead. ## [Related](#related) - [TLS compatibility — feature overview](https://webhookrelay.com/docs/webhooks/tls-ssl-errors/features/tls-compatibility/) - [Durable webhooks — reliable delivery with automatic retries](https://webhookrelay.com/docs/webhooks/tls-ssl-errors/docs/webhooks/durable-webhooks/) - [Custom webhook domains](https://webhookrelay.com/docs/webhooks/tls-ssl-errors/docs/webhooks/custom-domains/) - [Pricing](https://webhookrelay.com/docs/webhooks/tls-ssl-errors/pricing/) Did this page help you? --- --- title: Durable webhooks: reliable delivery with automatic retries meta: "og: title": "Durable webhooks: reliable delivery with automatic retries" description: Make webhook delivery reliable with durable retries. Webhook Relay persists every event and retries with exponential backoff for up to 30 days. url: https://webhookrelay.com/docs/webhooks/durable-webhooks.md file: /docs/webhooks/durable-webhooks.md --- ![Stripes](https://webhookrelay.com/docs/webhooks/durable-webhooks/images/stripes.svg) Documentation **Fundamentals** # **Durable webhooks: reliable delivery with automatic retries** Make webhook delivery reliable with durable retries. Webhook Relay persists every event and retries with exponential backoff for up to 30 days. A **durable webhook** is one that does not get lost when the receiving endpoint is having a bad moment. Instead of a single best-effort `POST`, the delivery is **persisted first** and then retried — patiently, with exponential backoff — until your endpoint accepts it or a deadline passes. This guide explains how durable webhook delivery works, how to turn it on for any destination in Webhook Relay, and how to design a receiver that handles retries safely. For the product overview, see the [durable retries feature page](https://webhookrelay.com/docs/webhooks/durable-webhooks/features/durable-retries/). The timeline below shows what durability buys you: a batch of 50 webhooks to a flaky endpoint, all converging on **delivered** over time. Press **play** (or scrub), and switch to **Retry attempts** to see the retry effort behind the curve. Durable delivery · live convergence 50/ 50 delivered 100%at 95 min 0 25 50 0m 15m 30m 45m 60m 75m 90m handoff 1-hour guarantee DeliveredRetryingAttempts50 webhooks · 275 attempts · 100% by ~73 min ## [Why webhook delivery fails](#why-webhook-delivery-fails) Webhooks arrive whenever the sender decides to send them — rarely when your server is at its best. Common reasons a delivery fails: - A **deploy or restart** takes the endpoint down for a few seconds. - A **database hiccup**, a slow query or a dependency timing out returns a `500`. - A short **network blip** or DNS failure between the sender and you. - The endpoint lives on **private infrastructure** that was briefly unreachable. Webhooks are an **at-least-once** delivery mechanism: senders try, and if they don't get a `2xx` back they may try again — or they may not. The retry behaviour you actually get depends entirely on the provider, and most of them give up quickly. ## [How long do providers retry, really?](#how-long-do-providers-retry-really) | Provider | Built-in retry behaviour | | --- | --- | | **Stripe** | Exponential backoff for up to ~3 days, then the event is dropped. | | **Shopify** | ~19 attempts over 48 hours, after which the webhook subscription is removed. | | **GitHub** | No automatic retries — failed deliveries must be redelivered by hand. | | **Most SaaS / internal senders** | A handful of attempts over a few minutes, or none at all. | Those windows are short, inconsistent, and impossible to change. Put Webhook Relay in front and every destination gets the **same** durable safety net regardless of who is sending — up to **30 days** of retries — including endpoints on localhost or behind a firewall that the original provider could never reach. ## [How durable retries work](#how-durable-retries-work) When durable delivery is enabled on a destination, every webhook goes through the same lifecycle: 1. **Persist first.** The moment an event reaches Webhook Relay it is written to durable storage — before any delivery is attempted. It now survives crashes, restarts and deploys on both ends. 2. **Fast retries.** Delivery is attempted immediately. Transient blips usually clear within the first few attempts, with no delay you'd notice. Fast retries run for the **handoff** window (15 minutes by default). 3. **Handoff to durable retry.** If the destination is still failing after the handoff window, delivery is handed to the durable retry engine, which keeps trying on your chosen schedule. 4. **Exponential backoff.** Each retry waits a little longer than the last, so a struggling server gets room to recover instead of being hammered while it's already down. 5. **Deadline.** Retries continue until the event is delivered or the schedule's deadline is reached. You can watch the whole thing happen: a delivery that is waiting for its next attempt shows up in your logs as **`stalled`**, with the time of the next retry, and flips to **`sent`** the moment it lands. ### [Retry schedules](#retry-schedules) Pick a schedule per destination based on how long you want Webhook Relay to keep trying: | Schedule | Total window | Best for | | --- | --- | --- | | **Seconds** | ~25 minutes | Endpoints that only ever blip — quick, persistent retries. | | **Medium** | ~16 hours | Outages measured in hours — outage tolerant. | | **Long** | ~30 days | A destination that might come back next week and still needs its events. | Two extra controls fine-tune the behaviour: - **Handoff after** — how long fast retries run before switching to durable retry (default **15 minutes**). - **Deadline** — total time before giving up. Leave it at `0` to use the schedule default (~16 h for medium, ~30 d for long). ## [Turn on durable delivery](#turn-on-durable-delivery) Durable delivery is configured per output destination. 1. Open your bucket and select the **output destination** you want to make durable. 2. In **Delivery controls**, make sure **Retries** is enabled. 3. Open **Durable delivery** and switch it on. 4. Choose a **Retry schedule** (Seconds, Medium or Long), optionally adjust **Handoff after** and **Deadline**, and **Save**. That's it — new webhooks to that destination are now persisted and retried on the schedule you picked. **Pair durable retries with **[throttling](https://webhookrelay.com/docs/webhooks/durable-webhooks/features/throttling/)** to control **_how fast_** retries reach a recovering server. Durable retries decide how long to keep trying; throttling decides the pace, so you never finish off a server that's only just getting back on its feet.** The configuration is stored on the output and visible through the API as a `durability` block: ``` { "name": "my-destination", "destination": "https://api.example.com/webhooks", "durability": { "enabled": true, "schedule": "medium", "handoff_after": 900000000000 } } ``` ## [Design your receiver for retries](#design-your-receiver-for-retries) Because durable delivery is **at-least-once**, the same event can legitimately arrive more than once — for example, your endpoint processed the request but the `200` was lost on the way back, so Webhook Relay retries. A correct receiver is **idempotent**: processing the same event twice has the same effect as processing it once. The standard pattern is to deduplicate on a stable **event id**: ``` // Express example app.post('/webhooks', async (req, res) => { const id = req.body.id // a stable id from the sender if (await alreadyProcessed(id)) { return res.sendStatus(200) // seen it — acknowledge and move on } try { await handleEvent(req.body) await markProcessed(id) res.sendStatus(200) // 2xx => Webhook Relay marks it delivered } catch (err) { res.sendStatus(500) // 5xx => Webhook Relay will retry later } }) ``` Guidelines for a retry-friendly endpoint: - **Return `2xx` only when you've safely accepted the event.** Any `5xx` (or a timeout) tells Webhook Relay to keep the event and retry. - **Acknowledge fast, work later.** If processing is slow, store the event and return `200` immediately, then process out of band — otherwise the request may time out and be retried unnecessarily. - **Key on the event id**, not the payload contents, so retries of the exact same event are recognised. ## [Watch deliveries converge](#watch-deliveries-converge) Open the bucket's request log to see delivery in real time. Each attempt shows its status: - **`sent`** — delivered, the endpoint returned `2xx`. - **`stalled`** — failed so far, waiting for its next retry (the next attempt time is shown). As endpoints recover, stalled deliveries flip to sent and the whole batch converges on **successfully delivered** — exactly the curve shown in the convergence timeline at the top of this page. No manual reconciliation, no lost events. ### [A live demo you can run](#a-live-demo-you-can-run) [**flakey-script**](https://github.com/webhookrelay/flakey-script) is a small open-source receiver that fails ~80% of the time on purpose, then always succeeds once an event is an hour old. Point a Webhook Relay bucket at it with durable retries enabled and watch every webhook retry and eventually land — even if you turn the receiver off for a while, which durable retries simply treat as another outage to recover from. See the [launch walkthrough](https://webhookrelay.com/docs/webhooks/durable-webhooks/blog/durable-webhook-retries-demo/) for the full setup. ## [Works for public and internal destinations](#works-for-public-and-internal-destinations) Durable retries cover both kinds of destination: - **Public HTTPS endpoints** — your API, a partner's API, any SaaS webhook URL. - **[Internal destinations](https://webhookrelay.com/docs/webhooks/durable-webhooks/features/webhook-to-internal-server/)** — services behind your firewall or on localhost reached through the Webhook Relay agent, even ones that were offline when the event arrived. If a destination is unreachable for hours, the events simply wait in durable storage and deliver the moment it comes back. ## [Frequently asked questions](#frequently-asked-questions) ### [What is a durable webhook?](#what-is-a-durable-webhook) A durable webhook is a delivery that is saved to persistent storage before it is attempted and then retried automatically until the receiving endpoint accepts it (or a deadline passes). Unlike a plain best-effort `POST`, a durable webhook is not lost when the endpoint is briefly down, slow, or returning errors. ### [How long will Webhook Relay retry a failed webhook?](#how-long-will-webhook-relay-retry-a-failed-webhook) It depends on the schedule you choose per destination: about 25 minutes (Seconds), about 16 hours (Medium), or up to 30 days (Long). You can also set an explicit deadline. ### [What is exponential backoff?](#what-is-exponential-backoff) Exponential backoff means each retry waits longer than the previous one (for example a few seconds, then minutes, then hours). It gives a struggling endpoint time to recover instead of hammering it with rapid retries while it's already failing. ### [How do I avoid processing the same webhook twice?](#how-do-i-avoid-processing-the-same-webhook-twice) Durable delivery is at-least-once, so make your handler idempotent: deduplicate on a stable event id and skip events you've already processed. Return `2xx` once an event is safely accepted, and a `5xx` to ask for a retry. ### [Does this work for endpoints on localhost or a private network?](#does-this-work-for-endpoints-on-localhost-or-a-private-network) Yes. Webhook Relay forwards to [internal destinations](https://webhookrelay.com/docs/webhooks/durable-webhooks/features/webhook-to-internal-server/) through its agent, and durable retries queue events while a private endpoint is offline, delivering them when it returns. ## [Related](#related) - [Durable retries — feature overview](https://webhookrelay.com/docs/webhooks/durable-webhooks/features/durable-retries/) - [Throttling — control delivery speed](https://webhookrelay.com/docs/webhooks/durable-webhooks/features/throttling/) - [Forwarding to internal services](https://webhookrelay.com/docs/webhooks/durable-webhooks/features/webhook-to-internal-server/) - [Polling webhooks with /v1/events](https://webhookrelay.com/docs/webhooks/durable-webhooks/docs/webhooks/polling-webhooks/) Did this page help you? --- --- title: Multi-factor authentication (MFA) | WebhookRelay meta: "og: title": "Multi-factor authentication (MFA)" description: Add multi-factor authentication (MFA) to your Webhook Relay account for an extra layer of login security. Available on every plan, including the free tier. url: https://webhookrelay.com/docs/account/mfa.md file: /docs/account/mfa.md --- ![Stripes](https://webhookrelay.com/docs/account/mfa/images/stripes.svg) Documentation **Fundamentals** # **Multi-factor authentication (MFA)** Add multi-factor authentication (MFA) to your Webhook Relay account for an extra layer of login security. Available on every plan, including the free tier. Multi-factor authentication (MFA), sometimes called two-factor authentication (2FA), adds a second step to your login. After your password you also enter a one-time code from an authenticator app, so a leaked or guessed password is no longer enough to access your account. MFA is available on **every plan, including the free tier** — there is no need to upgrade to secure your account. ## [How it works](#how-it-works) Webhook Relay uses **app-based, time-based one-time passwords (TOTP)** — the same standard supported by Google Authenticator, 1Password, Authy, Microsoft Authenticator and most password managers. Your authenticator app and Webhook Relay share a secret once, during setup, and from then on the app generates a fresh 6-digit code every 30 seconds. Nothing is sent over SMS, so there is no SIM-swap risk. ## [Enable MFA](#enable-mfa) 1. Open your [account details page](https://my.webhookrelay.com/account) and go to the **Security** section. 2. Choose **Enable two-factor authentication**. A QR code and a setup key are shown. 3. In your authenticator app, scan the QR code (or type the setup key manually). 4. Enter the 6-digit code from the app to confirm the two are in sync. 5. **Save your recovery codes** somewhere safe (see below), then finish. From the next sign-in onwards, you'll be asked for a code from your app after entering your password. **Set up your authenticator app on a device you keep — not the same single device you might lose access to. A password manager that syncs across your devices is a good place to store TOTP secrets.** ## [Recovery codes](#recovery-codes) When you enable MFA you are given a set of one-time **recovery codes**. Each code lets you sign in once if you don't have your authenticator app — for example if your phone is lost, stolen or reset. - Store them in a password manager or another safe place, **not** only on the device that runs your authenticator app. - Each recovery code works **once**. After you use one, cross it off. - You can regenerate a fresh set from the Security section at any time — doing so invalidates the old codes. If you run out of recovery codes **and** lose access to your authenticator app, contact [](https://webhookrelay.com/docs/account/mfa//mailto:info@webhookrelay.com) [info@webhookrelay.com](https://webhookrelay.com/docs/account/mfa//mailto:info@webhookrelay.com) from the email address on the account so we can verify ownership and help you regain access. ## [Disable MFA](#disable-mfa) To turn MFA off, open the **Security** section of your [account details page](https://my.webhookrelay.com/account) and disable two-factor authentication. You'll be asked to confirm with your password or a current code. We recommend keeping MFA enabled. ## [MFA and teams](#mfa-and-teams) If you invite [team members or sub-accounts](https://webhookrelay.com/docs/account/mfa/docs/account/team/), each user enables MFA on **their own** login independently — protecting your account is up to every member who can access it. For organisation-wide enforcement and SSO (SAML with Okta, Active Directory and similar), see our [Enterprise plan](https://webhookrelay.com/docs/account/mfa/pricing/). ## [Related](#related) - [Account management](https://webhookrelay.com/docs/account/mfa/docs/account/account-management/) - [Teams and sub-accounts](https://webhookrelay.com/docs/account/mfa/docs/account/team/) - [Security & technology overview](https://webhookrelay.com/docs/account/mfa/docs/security/) Did this page help you? --- --- title: Autostart (Linux) | WebhookRelay meta: "og: title": "Autostart (Linux)" description: Learn how to configure background service so that Webhook Relay agent connects on Linux server startup url: https://webhookrelay.com/docs/installation/autostart-linux.md file: /docs/installation/autostart-linux.md --- ![Stripes](https://webhookrelay.com/docs/installation/autostart-linux/images/stripes.svg) Documentation **Fundamentals** # **Autostart (Linux)** Learn how to configure background service so that Webhook Relay agent connects on Linux server startup ## [Prerequisites](#prerequisites) - Windows machine - [Webhook Relay account](https://my.webhookrelay.com) ## [Install relay client](#install-relay-client) Download and install the relay client: ``` curl https://my.webhookrelay.com/webhookrelay/downloads/install-cli.sh | bash ``` Create a config file in `/etc/webhookrelay/config.yaml`: ``` sudo mkdir -p /etc/webhookrelay ``` With contents (get your key and secret from [here](https://my.webhookrelay.com/tokens)): ``` vim /etc/webhookrelay/config.yaml ``` ``` version: "v1" key: your-secret-key # will be encrypted on startup secret: your-secret # will be encrypted on startup buckets: - my-bin ``` To install the service, you will need to use `sudo` and provide a full path to relay configuration file: ``` sudo relay service install -c /etc/webhookrelay/config.yaml ``` To specify credentials during install: ``` sudo relay service \ install -c /etc/webhookrelay/config.yaml \ --key [YOUR KEY] \ --secret [YOUR SECRET] ``` ### [Troubleshooting](#troubleshooting) To view service logs, use `journalctl`: ``` journalctl -u relay.service -f ``` Did this page help you? --- --- title: CLI | WebhookRelay meta: "og: title": CLI description: Learn how to install relay CLI on MacOS, Linux and Windows to start forwarding webhooks to your internal services and open tunnels to expose your services url: https://webhookrelay.com/docs/installation/cli.md file: /docs/installation/cli.md --- ![Stripes](https://webhookrelay.com/docs/installation/cli/images/stripes.svg) Documentation **Fundamentals** # **CLI** Learn how to install relay CLI on MacOS, Linux and Windows to start forwarding webhooks to your internal services and open tunnels to expose your services ## [Download](#download) Webhook Relay provides an official command client for quick configuration of webhook forwarding, tunnels and can also provision authentication tokens. It provides batteries-included agent for developing and testing workflows. It takes only a few seconds for it to start running with one-way HTTP request forwarding, bidirectional tunnels, and CLI for managing your resources. ## [Linux and MacOS users](#linux-and-macos-users) ``` curl https://my.webhookrelay.com/webhookrelay/downloads/install-cli.sh | bash ``` ## [Windows](#windows) ``` iwr https://my.webhookrelay.com/webhookrelay/downloads/install-cli.ps1 -useb | iex ``` ## [FreeBSD](#freebsd) Download the binary: ``` # For amd64 sudo fetch -o /usr/local/bin/relay https://storage.cloud.google.com/webhookrelay/downloads/relay-freebsd-amd64 # For i386 sudo fetch -o /usr/local/bin/relay https://storage.cloud.google.com/webhookrelay/downloads/relay-freebsd-386 ``` Give it permissions to execute and update itself: ``` sudo chmod +wx /usr/local/bin/relay ``` ## [Authentication](#authentication) First, open the dashboard [https://my.webhookrelay.com/](https://my.webhookrelay.com/) and either register or login. Then, go to the [https://my.webhookrelay.com/tokens](https://my.webhookrelay.com/tokens) page and click on "create token" and follow the instructions: ![create token](https://webhookrelay.com/docs/installation/cli/images/docs/installation/token-create.png) Once created, you can test it by running ``` relay bucket ls ``` Which should return a list of your buckets. ## [Download binaries directly](#download-binaries-directly) If you wish to skip using our installation script, you can find individual executables here: ``` # Linux x86-64 (64-bit) https://storage.googleapis.com/webhookrelay/downloads/relay-linux-amd64 # Linux x86 (32-bit) https://storage.googleapis.com/webhookrelay/downloads/relay-linux-386 # Linux aarch (64-bit) https://storage.googleapis.com/webhookrelay/downloads/relay-linux-aarch64 # Linux arm (32-bit) https://storage.googleapis.com/webhookrelay/downloads/relay-linux-arm # Windows (64-bit) https://storage.googleapis.com/webhookrelay/downloads/relay-windows-amd64.exe # Windows (32-bit) https://storage.googleapis.com/webhookrelay/downloads/relay-windows-386.exe ``` ## [Changelog](#changelog) To view what has changed, please visit [our changelog](https://webhookrelay.com/docs/installation/cli/changelog/). Did this page help you? --- --- title: HTTP proxy configuration | WebhookRelay meta: "og: title": "HTTP proxy configuration" description: How to configure relay or webhookrelayd agent to work behind a proxy url: https://webhookrelay.com/docs/installation/behind-proxy.md file: /docs/installation/behind-proxy.md --- ![Stripes](https://webhookrelay.com/docs/installation/behind-proxy/images/stripes.svg) Documentation **Fundamentals** # **HTTP proxy configuration** How to configure relay or webhookrelayd agent to work behind a proxy When using `relay` CLI (`webhookrelayd` container also respects these variables) to specify a proxy that the application should use to connect, set environment variables: ``` export HTTP_PROXY=http://hostname:port/ export HTTPS_PROXY=http://hostname:port/ ``` If the proxy will be performing MITM for HTTPS with untrusted certificate, specify **RELAY_INSECURE** variable to turn **off** TLS verification: ``` export RELAY_INSECURE=true ``` ## [Forwarding webhooks behind a proxy](#forwarding-webhooks-behind-a-proxy) If your proxy cannot forward [GRPC](https://grpc.io) traffic, supply **--ws** flag to `relay forward` command: ``` relay forward --ws ``` This command will switch webhook transport from **GRPC** to **WebSocket**. The only downside of using WebSocket transport is that you will not be able to view response status code through the web UI as they are not returned. This will be changed in future and underlying protocols will offer same funcionality. ## [Tunneling behind a proxy](#tunneling-behind-a-proxy) When using tunnel, `relay` CLI and `webhookrelayd` Docker container will create a TCP connection to either `tunnel.webrelay.io:9800` or other hosts based on the tunnelling region. Ensure that this URL is reachable. Did this page help you? --- --- title: Autostart (MacOS) | WebhookRelay meta: "og: title": "Autostart (MacOS)" description: Learn how to configure background service so that Webhook Relay agent connects on MacOS startup url: https://webhookrelay.com/docs/installation/autostart-macos.md file: /docs/installation/autostart-macos.md --- ![Stripes](https://webhookrelay.com/docs/installation/autostart-macos/images/stripes.svg) Documentation **Fundamentals** # **Autostart (MacOS)** Learn how to configure background service so that Webhook Relay agent connects on MacOS startup For MacOS we recommend using [Docker](https://webhookrelay.com/docs/installation/autostart-macos/docs/installation/docker/) installation method as you can start a container with `-d` flag to run the agent in the background. ## [Prerequisites](#prerequisites) - MacOS machine - [Webhook Relay account](https://my.webhookrelay.com) ## [Install relay client](#install-relay-client) Download and install the relay client: ``` curl https://my.webhookrelay.com/webhookrelay/downloads/install-cli.sh | bash ``` Create a config file in `/etc/webhookrelay/config.yaml`: ``` sudo mkdir -p /etc/webhookrelay ``` With contents (get your key and secret from [here](https://my.webhookrelay.com/tokens)): ``` vim /etc/webhookrelay/config.yaml ``` ``` version: "v1" key: your-secret-key # will be encrypted on startup secret: your-secret # will be encrypted on startup buckets: - my-bin ``` To install the service provide a full path to relay configuration file: ``` relay service install -c /etc/webhookrelay/config.yaml -u your-user ``` Did this page help you? --- --- title: Docker Compose | WebhookRelay meta: "og: title": "Docker Compose" description: How to use Webhook Relay client with Docker Compose to start forwarding webhooks to your internal services and open tunnels to expose your services url: https://webhookrelay.com/docs/installation/docker-compose.md file: /docs/installation/docker-compose.md --- ![Stripes](https://webhookrelay.com/docs/installation/docker-compose/images/stripes.svg) Documentation **Fundamentals** # **Docker Compose** How to use Webhook Relay client with Docker Compose to start forwarding webhooks to your internal services and open tunnels to expose your services Docker Compose is an excellent option to run multiple containers together. You can have a perfect development environment that receives webhooks directly from Stripe, Github and other services. ## [Prerequisites](#prerequisites) - Docker, installation instructions: [https://docs.docker.com/engine/install/](https://docs.docker.com/engine/install/) - Webhook Relay account, get your token here: [https://my.webhookrelay.com/tokens](https://my.webhookrelay.com/tokens) ## [Forward webhooks](#forward-webhooks) 1. Go to [https://my.webhookrelay.com/buckets](https://my.webhookrelay.com/buckets) and create a bucket (we will call it "**my-bucket**" in this example) 2. Configure output destination (another container or IP address where you want to forward) 3. Create a _docker-compose.yml_ file: ``` version: '3.2' services: relay: container_name: webhookrelay image: webhookrelay/webhookrelayd:latest network_mode: host # required if you want to access other services running on localhost (otherwise localhost would be inside this container) restart: always environment: # Authentication - RELAY_KEY=${RELAY_KEY} - RELAY_SECRET=${RELAY_SECRET} # buckets list to subscribe - BUCKETS=${BUCKETS} ``` 1. Create _.env_ file: ``` RELAY_KEY="your-access-token-key" RELAY_SECRET="your-access-token-secret" BUCKETS=my-bucket ``` 1. Start Docker Compose: ``` docker-compose up -d ``` ## [Open a tunnel](#open-a-tunnel) 1. Go to [https://my.webhookrelay.com/tunnels](https://my.webhookrelay.com/tunnels) and create a tunnel with your desired destination 2. Create a _docker-compose.yml_ file: ``` version: '3.2' services: relay: container_name: webhookrelay image: webhookrelay/webhookrelayd:latest network_mode: host command: - --mode - tunnel restart: always environment: # Authentication - RELAY_KEY=${RELAY_KEY} - RELAY_SECRET=${RELAY_SECRET} # One or more tunnels must be set in the .env file - TUNNELS=${TUNNELS} ``` 1. Create _.env_ file: ``` RELAY_KEY="your-access-token-key" RELAY_SECRET="your-access-token-secret" TUNNELS=your-tunnel ``` 1. Start Docker Compose: ``` docker-compose up -d ``` Did this page help you? --- --- title: Kubernetes | WebhookRelay meta: "og: title": Kubernetes description: How to use Webhook Relay client with Kubernetes to start forwarding webhooks to your internal services and open tunnels to expose your services url: https://webhookrelay.com/docs/installation/kubernetes.md file: /docs/installation/kubernetes.md --- ![Stripes](https://webhookrelay.com/docs/installation/kubernetes/images/stripes.svg) Documentation **Fundamentals** # **Kubernetes** How to use Webhook Relay client with Kubernetes to start forwarding webhooks to your internal services and open tunnels to expose your services Webhook Relay can help you receive webhooks in your internal services. To achieve that you can use: 1. [Webhook Relay operator](#Option-1-Webhook-Relay-Operator-recommended) - recommended way to forward webhooks to Kubernetes clusters. Handles agent deployment and routing configuration. 2. [A sidecar container](#Option-2-Sidecar) - does not automatically configure routing. 3. [A standalone deployment](#Option-3-Separate-deployment) - does not automatically configure routing. 4. [Webhook Relay ingress controller](#Option-4-Ingress-Controller) - recommended way to open bidirectional tunnels to expose services directly from your Kubernetes cluster such as Grafana, Prometheus, etc. Since container is stateless and only requires your access key & secret, deploying and running it is extremely easy. Recommended way to deploy Webhook Relay into your cluster is using the official operator. ## [Option 1: Webhook Relay Operator (recommended)](#option-1-webhook-relay-operator-recommended) Webhook Relay operator not only deploys and manages agent containers that subscribe and forward webhooks but it also configures buckets, inputs (your public endpoints) and outputs (forwarding destinations). ### [Install](#install) Prerequisites: - [Helm](https://docs.helm.sh/using_helm/#installing-helm) - [Webhook Relay account](https://my.webhookrelay.com) - Kubernetes You need to add this Chart repo to Helm: ``` helm repo add webhookrelay https://charts.webhookrelay.com helm repo update ``` Get access token from [here](https://my.webhookrelay.com/tokens). Once you click on 'Create Token', it will generate it and show a helper to set environment variables: ``` export RELAY_KEY=*****-****-****-****-********* export RELAY_SECRET=********** ``` Install through Helm: ``` helm upgrade --install webhookrelay-operator --namespace=default webhookrelay/webhookrelay-operator \ --set credentials.key=$RELAY_KEY --set credentials.secret=$RELAY_SECRET ``` ### [Usage](#usage) Once the operator is deployed, to start receiving webhooks you will need to create a [Custom Resource](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/) (usually called just 'CR'). It's a short yaml file that describes your public endpoint characteristics and specifies where to forward the webhooks: ``` # cr.yaml apiVersion: forward.webhookrelay.com/v1 kind: WebhookRelayForward metadata: name: example-forward spec: buckets: - name: k8s-operator inputs: - name: public-endpoint description: "Public endpoint, supply this to the webhook producer" responseBody: "OK" responseStatusCode: 200 outputs: - name: webhook-receiver destination: http://destination:5050/webhooks ``` ``` kubectl apply -f cr.yaml ``` ### [Uninstall](#uninstall) To remove the agent that is forwarding the webhooks, remove the CR that created it: ``` kubectl delete -f cr.yaml ``` To remove operator, use standard Helm command to uninstall the operator. ``` helm uninstall webhookrelay-operator ``` ## [Option 2: Sidecar](#option-2-sidecar) First, go to [https://my.webhookrelay.com/tokens](https://my.webhookrelay.com/tokens) and create a token key & secret pair. Then, create a Kubernetes secret: ``` kubectl create secret generic webhookrelay-credentials --from-literal=key=[access key] --from-literal=secret=[access secret] ``` Once the secret is created, you can deploy webhookrelayd container either as a sidecar or a standalone container. Webhookrelayd agent can be easily deployed as a sidecar. This way requests can be forwarded to the service through localhost: ``` apiVersion: apps/v1 kind: Deployment metadata: name: webhookrelay-deployment labels: app: webhookrelay spec: replicas: 1 selector: matchLabels: app: webhookrelay template: metadata: labels: app: webhookrelay spec: containers: - name: demo image: karolisr/webhook-demo:0.0.15 command: ["/bin/webhook-demo"] ports: - containerPort: 8090 # Webhook Relay sidecar - name: webhookrelayd image: webhookrelay/webhookrelayd:latest env: - name: KEY valueFrom: secretKeyRef: name: webhookrelay-credentials key: key - name: SECRET valueFrom: secretKeyRef: name: webhookrelay-credentials key: secret - name: BUCKETS value: bucket1,bucket2,bucket3 ``` ## [Option 3: Separate deployment](#option-3-separate-deployment) Webhook Relay container can also work as standalone deployment: ``` apiVersion: apps/v1 kind: Deployment metadata: name: webhookrelay-deployment labels: app: webhookrelay spec: replicas: 1 selector: matchLabels: app: webhookrelay template: metadata: labels: app: webhookrelay spec: containers: - name: webhookrelayd image: webhookrelay/webhookrelayd:latest env: - name: KEY valueFrom: secretKeyRef: name: webhookrelay-credentials key: key - name: SECRET valueFrom: secretKeyRef: name: webhookrelay-credentials key: secret - name: BUCKETS value: bucket1,bucket2,bucket3 ``` If agent is deployed as a separate deployment, the **output** destination should then be a service name. Repository can be found here: [https://github.com/webhookrelay/webhook-demo](https://github.com/webhookrelay/webhook-demo). ## [Option 4: Ingress Controller](#option-4-ingress-controller) Implements a [Kubernetes](https://kubernetes.io/) ingress controller using tunnels to connect a Web Relay managed URL (_[https://yoursubdomain.webrelay.io](https://yoursubdomain.webrelay.io)_) to a Kubernetes [service](https://kubernetes.io/docs/concepts/services-networking/service/) based on [ingress resources](https://kubernetes.io/docs/concepts/services-networking/ingress/#what-is-ingress). Single ingress controller can manage multiple tunnels and route to multiple namespaces. Deployment files and issue tracker is available on GitHub: [https://github.com/webrelay/ingress](https://github.com/webrelay/ingress) You can try out Web Relay ingress controller by creating a deployment from a hosted manifest, no clone or local install necessary. What you do need: - A Kubernetes cluster that has access to the Internet - `kubectl` configured with admin access to your cluster - `relay` CLI, installation instructions can be found [here](https://docs.webhookrelay.com/installation-options/installation-options/install-cli) ### [Installing](#installing) To add Web Relay ingress controller to your cluster, run: ``` relay ingress init ``` > Manifests are available here: [https://github.com/webrelay/ingress/tree/master/deployment](https://github.com/webrelay/ingress/tree/master/deployment) This command: - Creates `webrelay-ingress` namespace - Creates `webrelay` service account - Creates deployment with the controller - Creates cluster role and binding - Generates access key and secret for the Web Relay server and supplies them as a Kubernetes secret If RBAC isn't enabled on your cluster (for example, if you're on GKE with legacy authorization or Minikube without RBAC), run: ``` relay ingress init --no-rbac ``` > You can also generate tokens through the Web UI here [https://my.webhookrelay.com/tokens](https://my.webhookrelay.com/tokens) or `relay token create` command on the CLI. ### [Uninstalling Ingress Controller](#uninstalling-ingress-controller) To remove it, either delete the namespace where it was deployed or use: ``` relay ingress reset ``` Did this page help you? --- --- title: Docker container | WebhookRelay meta: "og: title": "Docker container" description: How to use Webhook Relay client with Docker to start forwarding webhooks to your internal services and open tunnels to expose your services url: https://webhookrelay.com/docs/installation/docker.md file: /docs/installation/docker.md --- ![Stripes](https://webhookrelay.com/docs/installation/docker/images/stripes.svg) Documentation **Fundamentals** # **Docker container** How to use Webhook Relay client with Docker to start forwarding webhooks to your internal services and open tunnels to expose your services ## [Prerequisites](#prerequisites) - Docker, installation instructions: [https://docs.docker.com/engine/install/](https://docs.docker.com/engine/install/) - Webhook Relay account, get your token here: [https://my.webhookrelay.com/tokens](https://my.webhookrelay.com/tokens) ## [Forward webhooks](#forward-webhooks) 1. Go to [https://my.webhookrelay.com/buckets](https://my.webhookrelay.com/buckets) and create a bucket and where you want to forward the webhooks 2. Go to the [tokens page](https://my.webhookrelay.com/tokens) and get your access key and secret ``` export RELAY_KEY= export RELAY_SECRET= ``` 1. Start a webhookrelayd agent: ``` docker run -d \ --name whr-relayd \ --restart always \ -e RELAY_KEY=${RELAY_KEY} \ -e RELAY_SECRET=${RELAY_SECRET} \ -e BUCKETS= \ webhookrelay/webhookrelayd:latest ``` If you are using self-signed certificates on your internal side, specify `INSECURE` environment variable to skip validation: ``` INSECURE=true ``` ## [Open a tunnel](#open-a-tunnel) 1. Go to [https://my.webhookrelay.com/tunnels](https://my.webhookrelay.com/tunnels) and create a tunnel with your desired destination 2. Start a bidirectional tunnel: ``` docker run --name whr-relayd \ --net host \ --restart always \ -d webhookrelay/webhookrelayd:latest \ --mode tunnel -t mytunnelname -k [access key] -s [access secret] ``` Here _webhookrelayd_ commands: - **--mode tunnel** indicates that it should start bidirectional tunnel - **-t mytunnelname** acts as a filter, it has to match the tunnel name that you have created previously - **-k access key** is your authentication token key - **-s access secret** is your authentication token secret You can also specify these details through environment variables: ``` KEY= SECRET= TUNNELS= REGION= ``` Did this page help you? --- --- title: Autostart (Windows) | WebhookRelay meta: "og: title": "Autostart (Windows)" description: Learn how to configure background service so that Webhook Relay agent connects on Windows server startup url: https://webhookrelay.com/docs/installation/autostart-windows.md file: /docs/installation/autostart-windows.md --- ![Stripes](https://webhookrelay.com/docs/installation/autostart-windows/images/stripes.svg) Documentation **Fundamentals** # **Autostart (Windows)** Learn how to configure background service so that Webhook Relay agent connects on Windows server startup ## [Prerequisites](#prerequisites) - Windows machine - [Webhook Relay account](https://my.webhookrelay.com) ## [Install relay client](#install-relay-client) Open PowerShell: ![opening powershell](https://webhookrelay.com/docs/installation/autostart-windows/images/docs/installation/windows/powershell.png) Download and install the relay client: ``` iwr https://my.webhookrelay.com/webhookrelay/downloads/install-cli.ps1 -useb | iex ``` You should see the following output: ![running command to install](https://webhookrelay.com/docs/installation/autostart-windows/images/docs/installation/windows/command.png) Now, create a file `config` in the C:\ProgramData\WebhookRelay directory: ![config file location](https://webhookrelay.com/docs/installation/autostart-windows/images/docs/installation/windows/config-file.png) With contents (get your key and secret from [here](https://my.webhookrelay.com/tokens)): ``` version: "v1" key: your-secret-key # will be encrypted on startup secret: your-secret # will be encrypted on startup buckets: - windows-bin ``` Then, install and start the service: ``` relay service install -c 'C:\ProgramData\WebhookRelay\config.txt' ``` The agent is now installed and will be run after a system reboot. To restart the service (if you change the configuration file): ``` relay service restart ``` ### [Troubleshooting](#troubleshooting) - To view the logs: ``` relay service install -c C:\ProgramData\WebhookRelay\config.txt --logs-output C:\ProgramData\WebhookRelay\relay.log ``` - If the service is not starting, check the logs in `C:\ProgramData\WebhookRelay\relay.log` Did this page help you? --- --- title: Multiple destinations | WebhookRelay meta: "og: title": "Multiple destinations" description: How to forward a webhook to a single public URL url: https://webhookrelay.com/docs/webhooks/public/multiple-destination-urls.md file: /docs/webhooks/public/multiple-destination-urls.md --- ![Stripes](https://webhookrelay.com/docs/webhooks/public/multiple-destination-urls/images/stripes.svg) Documentation **Fundamentals** # **Multiple destinations** How to forward a webhook to a single public URL _This section is under construction_ Did this page help you? --- --- title: Forward to public URL | WebhookRelay meta: "og: title": "Forward to public URL" description: How to forward a webhook to a single public URL url: https://webhookrelay.com/docs/webhooks/public/public-destination.md file: /docs/webhooks/public/public-destination.md --- ![Stripes](https://webhookrelay.com/docs/webhooks/public/public-destination/images/stripes.svg) Documentation **Fundamentals** # **Forward to public URL** How to forward a webhook to a single public URL To forward to a public URL, the fastest way is to use the helper from [home page](https://my.webhookrelay.com/new-public-destination): ![Forward to public URL](https://webhookrelay.com/docs/webhooks/public/public-destination/images/docs/webhooks/forward_helper.png) Follow the instructions to create a new public destination. Once created, you can edit the destination or configure additional settings such as authentication, webhook transformation, etc. Did this page help you? --- --- title: JWT authentication | WebhookRelay meta: "og: title": "JWT authentication" description: a helper JWT package is available to validate and authenticate webhooks url: https://webhookrelay.com/docs/webhooks/auth/jwt.md file: /docs/webhooks/auth/jwt.md --- ![Stripes](https://webhookrelay.com/docs/webhooks/auth/jwt/images/stripes.svg) Documentation **Fundamentals** # **JWT authentication** a helper JWT package is available to validate and authenticate webhooks ## [What are JWT tokens](#what-are-jwt-tokens) From [jwt.io](https://jwt.io/): JSON Web Token (JWT) is an open standard [RFC 7519](https://datatracker.ietf.org/doc/html/rfc7519/) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the **HMAC** algorithm) or a public/private key pair using **RSA** or **ECDSA**. In short, JWT tokens allow you to easily authenticate incoming HTTP (or any other data) requests. Webhook Relay provides a Mailgun package to easily send emails on various events. ### [Setting up signing secret](#setting-up-signing-secret) Both RSA and HMAC signature verification algorithms will need to have a key based on which to validate the tokens. Avoid specifying them directly in your function, create a config variable: ![specifying jwt signing secret in the config variables](https://webhookrelay.com/docs/webhooks/auth/jwt/images/docs/webhooks/auth/specifying-jwt-signing-secret.png) If you are using RSA public key, first encode it using base64. ### [Authenticating HTTP requests](#authenticating-http-requests) Most applications use a standard bearer token format when sending HTTP requests. This involves setting an Authorization header: ``` Authorization: Bearer ``` Webhook Relay's jwt package knows where to find it, so you only need to supply the signing key: ``` jwt = require('jwt') -- Importing jwt helper package local err = jwt.authenticate(cfg:GetValue("jwt-signing-key")) -- Your secret if err then error(err) end r:SetRequestBody("authenticated") ``` ### [Testing authentication](#testing-authentication) You can use [https://jwt.io](https://jwt.io) to create a valid JWT token with the same secret that you have added to the config variables. Add the token as a header, click on the "+" sign and then click on the "send" button: ![JWT authentication testing](https://webhookrelay.com/docs/webhooks/auth/jwt/images/docs/webhooks/auth/jwt-authentication-testing.png) If you change the secret either in the config variables or on the jwt generator, you should see an error: ![Failed authentication on webhook](https://webhookrelay.com/docs/webhooks/auth/jwt/images/docs/webhooks/auth/jwt-authentication-error.png) Once an error happens, webhook will not be forwarded further. ### [Custom JWT validation](#custom-jwt-validation) If your token is not set in the Authorization header, you can use a different function: ``` jwt = require('jwt') local err = jwt.validate("your-jwt-token-value-here", cfg:GetValue("jwt-signing-key")) if err then error(err) end r:SetRequestBody("authenticated") ``` ### [Supported algorithms](#supported-algorithms) Webhook Relay's JWT package supports: - HS - HMAC using SHA256/SHA384/SHA512 - RS - RSASSA-PKCS-v1.5 using SHA-256/SHA-384/SHA-512 - ECDSA using P-256 and SHA-256 - ECDSA using P-384 and SHA-384 - ECDSA using P-521 and SHA-512 - RSASSA-PSS using SHA256 and MGF1-SHA256 - RSASSA-PSS using SHA384 and MGF1-SHA384 - RSASSA-PSS using SHA512 and MGF1-SHA512 Did this page help you? --- --- title: Auth using request method | WebhookRelay meta: "og: title": "Auth using request method" description: How do I allow only POST requests through the input or output? url: https://webhookrelay.com/docs/webhooks/auth/http-method.md file: /docs/webhooks/auth/http-method.md --- ![Stripes](https://webhookrelay.com/docs/webhooks/auth/http-method/images/stripes.svg) Documentation **Fundamentals** # **Auth using request method** How do I allow only POST requests through the input or output? ## [Using Functions to filter](#using-functions-to-filter) How can you allow only POST requests go through? With Webhook Relay it's easy to do any kind of webhook filtering with Functions. ## [Setting up the function](#setting-up-the-function) Often webhooks are sent as POST requests. However, sometimes you might be getting other requests to this endpoint (GET, PATCH, etc.), to filter them out, create a function: ``` if r.RequestMethod ~= "POST" then -- request is not important, don't forward it r:StopForwarding() return end ``` And attach it to the output (Go to the Bucket settings, then click on the destination and then select **Transform** tab). ## [Testing the Function](#testing-the-function) Once added, POST requests will come through: ``` curl -X POST https://z2dc2rdlhajz826steaiig.hooks.webhookrelay.com ``` while PUT, GET and other request method webhooks: ``` curl -X POST https://z2dc2rdlhajz826steaiig.hooks.webhookrelay.com ``` ![allowing-post-requests](https://webhookrelay.com/docs/webhooks/auth/http-method/images/docs/webhooks/auth/allowing-post-requests.png) Did this page help you? --- --- title: Username and password | WebhookRelay meta: "og: title": "Username and password" description: How to set up authentication for webhooks. This guide shows you how to use basic username and password or token authentication. url: https://webhookrelay.com/docs/webhooks/auth/username-password.md file: /docs/webhooks/auth/username-password.md --- ![Stripes](https://webhookrelay.com/docs/webhooks/auth/username-password/images/stripes.svg) Documentation **Fundamentals** # **Username and password** How to set up authentication for webhooks. This guide shows you how to use basic username and password or token authentication. ## [Public endpoint authentication](#public-endpoint-authentication) To enable username and password authentication on all public endpoints that belong to a bucket, go to the authentication section: ![bucket authentication settings](https://webhookrelay.com/docs/webhooks/auth/username-password/images/docs/webhooks/auth/bucket-authentication.png) Then, you can either select "basic" or "token" authentication methods. ## [Basic authentication (username and password)](#basic-authentication-username-and-password) With basic authentication you will need to specify username and password. Unauthenticated requests to bucket's inputs will result in "Unauthorized" error: ``` curl https://hqzxx4bpayrk4sdrfifte6.hooks.webhookrelay.com Unauthorized ``` The endpoint now expects a request to have 'Authorization: Basic ' header. Tools like curl can help authenticate: ``` curl \ --user test-username:test-password \ https://hqzxx4bpayrk4sdrfifte6.hooks.webhookrelay.com ``` ## [Bearer (token) authentication](#bearer-token-authentication) To use bearer token authentication, select 'token' from the authentication menu and set your token value. In this case, to successfully send webhooks, you will need to set 'Authorization: Bearer ' header: ``` curl \ -H 'Authorization: Bearer very-secret' \ https://hqzxx4bpayrk4sdrfifte6.hooks.webhookrelay.com ``` Did this page help you? --- --- title: Make HTTP request | WebhookRelay meta: "og: title": "Make HTTP request" description: Making HTTP requests from Webhook Relay Functions url: https://webhookrelay.com/docs/webhooks/functions/make-http-request.md file: /docs/webhooks/functions/make-http-request.md --- ![Stripes](https://webhookrelay.com/docs/webhooks/functions/make-http-request/images/stripes.svg) Documentation **Fundamentals** # **Make HTTP request** Making HTTP requests from Webhook Relay Functions ![How to make an HTTP call from within the function](https://webhookrelay.com/docs/webhooks/functions/make-http-request/images/docs/webhooks/functions/function-http-call.png) Functions can make multiple HTTP request calls to any external server. Some of the uses cases: - Call 3rd party API to get additional authentication tokens before forwarding request. - Send data to the external service directly, without relying on the webhook. - Get additional data to the function so it can dynamically mutate the payload. ## [Using HTTP module](#using-http-module) To make an HTTP request from a function: ``` // making HTTP call const response = http.request("GET", "https://example.com") // parsing response body from the API const apiResponse = JSON.parse(response.body) ``` ``` -- importing HTTP package local http = require("http") -- making HTTP call response, err = http.request("GET", "https://example.com") if err then error(err) end -- parsing response body from the API local api_response, err = json.decode(response.body) if err then error(err) end ``` ## [Specify request body](#specify-request-body) You can also make a **POST**, **PUT**, **DELETE** requests to a 3rd party APIs: ``` const payload = JSON.stringify({ action: "create_record", user: "example", }) const resp = http.request("POST", "http://example.com/api", { body: payload }) ``` ``` -- importing HTTP and JSON packages local json = require("json") local http = require("http") local payload, err = json.encode({ action = "create_record", user = "example", }) if err then error(err) end local resp, err = http.request("POST", "http://example.com/api", { body = payload}) if err then error(err) end ``` ## [Reading response body](#reading-response-body) To read response body: ``` // making HTTP call const response = http.request("GET", "https://example.com") // received JSON string: // { // "firstname": "luke", // "lastname": "skywalker" // } // parsing response body from the API const apiResponse = JSON.parse(response.body) // access values like 'apiResponse.firstname' ``` ``` -- importing HTTP package local http = require("http") -- making HTTP call response, err = http.request("GET", "https://example.com") if err then error(err) end -- received JSON string: -- { -- "firstname": "luke", -- "lastname": "skywalker" -- } -- parsing response body from the API local api_response, err = json.decode(response.body) if err then error(err) end -- access values like 'api_response.firstname' ``` ## [Query, headers, timeout](#query-headers-timeout) To specify query, timeout and headers: ``` // making HTTP call with options const response = http.request("GET", "https://example.com", { query: "page=1", timeout: "5s", headers: { Accept: "*/*", Authorization: "Basic 123456" } }) // parsing response body from the API const apiResponse = JSON.parse(response.body) ``` ``` -- importing HTTP package local http = require("http") -- specifying headers local headers = {} headers["Authorization"] = "Basic " .. "123456" -- making HTTP call response, err = http.request("GET", "https://example.com", { query="page=1", timeout="5s", headers={ Accept="*/*" } }) if err then error(err) end -- parsing response body from the API local api_response, err = json.decode(response.body) if err then error(err) end ``` ## [HTTP module API reference](#http-module-api-reference) #### [http.delete(url , options)](#httpdeleteurl-options) **Attributes** | Name | Type | Description | | --- | --- | --- | | url | String | URL of the resource to load | | options | Table | Additional options | **Options** | Name | Type | Description | | --- | --- | --- | | query | String | URL encoded query params | | cookies | Table | Additional cookies to send with the request | | headers | Table | Additional headers to send with the request | **Returns** [http.response](#http-response) or (nil, error message) #### [http.get(url , options)](#httpgeturl-options) **Attributes** | Name | Type | Description | | --- | --- | --- | | url | String | URL of the resource to load | | options | Table | Additional options | **Options** | Name | Type | Description | | --- | --- | --- | | query | String | URL encoded query params | | cookies | Table | Additional cookies to send with the request | | headers | Table | Additional headers to send with the request | **Returns** [http.response](#http-response) or (nil, error message) #### [http.head(url , options)](#httpheadurl-options) **Attributes** | Name | Type | Description | | --- | --- | --- | | url | String | URL of the resource to load | | options | Table | Additional options | **Options** | Name | Type | Description | | --- | --- | --- | | query | String | URL encoded query params | | cookies | Table | Additional cookies to send with the request | | headers | Table | Additional headers to send with the request | **Returns** [http.response](#http-response) or (nil, error message) #### [http.patch(url , options)](#httppatchurl-options) **Attributes** | Name | Type | Description | | --- | --- | --- | | url | String | URL of the resource to load | | options | Table | Additional options | **Options** | Name | Type | Description | | --- | --- | --- | | query | String | URL encoded query params | | cookies | Table | Additional cookies to send with the request | | body | String | Request body. | | form | String | Deprecated. URL encoded request body. This will also set the `Content-Type` header to `application/x-www-form-urlencoded` | | headers | Table | Additional headers to send with the request | **Returns** [http.response](#http-response) or (nil, error message) #### [http.post(url , options)](#httpposturl-options) **Attributes** | Name | Type | Description | | --- | --- | --- | | url | String | URL of the resource to load | | options | Table | Additional options | **Options** | Name | Type | Description | | --- | --- | --- | | query | String | URL encoded query params | | cookies | Table | Additional cookies to send with the request | | body | String | Request body. | | form | String | Deprecated. URL encoded request body. This will also set the `Content-Type` header to `application/x-www-form-urlencoded` | | headers | Table | Additional headers to send with the request | **Returns** [http.response](#http-response) or (nil, error message) #### [http.put(url , options)](#httpputurl-options) **Attributes** | Name | Type | Description | | --- | --- | --- | | url | String | URL of the resource to load | | options | Table | Additional options | **Options** | Name | Type | Description | | --- | --- | --- | | query | String | URL encoded query params | | cookies | Table | Additional cookies to send with the request | | body | String | Request body. | | form | String | Deprecated. URL encoded request body. This will also set the `Content-Type` header to `application/x-www-form-urlencoded` | | headers | Table | Additional headers to send with the request | **Returns** [http.response](#http-response) or (nil, error message) #### [http.request(method, url , options)](#httprequestmethod-url-options) **Attributes** | Name | Type | Description | | --- | --- | --- | | method | String | The HTTP request method | | url | String | URL of the resource to load | | options | Table | Additional options | **Options** | Name | Type | Description | | --- | --- | --- | | query | String | URL encoded query params | | cookies | Table | Additional cookies to send with the request | | body | String | Request body. | | form | String | Deprecated. URL encoded request body. This will also set the `Content-Type` header to `application/x-www-form-urlencoded` | | headers | Table | Additional headers to send with the request | **Returns** [http.response](#http-response) or (nil, error message) ### [http.response](#httpresponse) The `http.response` table contains information about a completed HTTP request. **Attributes** | Name | Type | Description | | --- | --- | --- | | body | String | The HTTP response body | | body_size | Number | The size of the HTTP reponse body in bytes | | headers | Table | The HTTP response headers | | cookies | Table | The cookies sent by the server in the HTTP response | | status_code | Number | The HTTP response status code | | url | String | The final URL the request ended pointing to after redirects | Did this page help you? --- --- title: HMAC | WebhookRelay meta: "og: title": HMAC description: HMAC is the most popular authentication and message security method used on webhook requests. Learn how to validate HMAC signatures url: https://webhookrelay.com/docs/webhooks/auth/hmac.md file: /docs/webhooks/auth/hmac.md --- ![Stripes](https://webhookrelay.com/docs/webhooks/auth/hmac/images/stripes.svg) Documentation **Fundamentals** # **HMAC** HMAC is the most popular authentication and message security method used on webhook requests. Learn how to validate HMAC signatures [HMAC](https://en.wikipedia.org/wiki/HMAC) for webhooks can be calculated using SHA256 and SHA512 algorithms combined with the data and the key. You can also use MD5, SHA1 but those methods are no longer recommended. It may be used to simultaneously verify both the data integrity and the authenticity of a message. In this example we will use [GitHub webhooks](https://docs.github.com/en/webhooks/using-webhooks/securing-your-webhooks). Other providers that use HMAC authentication: - [Shopify](https://shopify.dev/docs/apps/webhooks/configuration/https#step-5-verify-the-webhook) - [Slack](https://api.slack.com/authentication/verifying-requests-from-slack) - [Square](https://developer.squareup.com/docs/webhooks/step3validate) While this example is using GitHub, the same approach can be used for other providers. You will only need to modify two lines of the function when using other providers: 1. `local signature_header = 'X-Hub-Signature-256'` will need to include your provider's header name 2. `if "sha256=" .. calculated_hmac ~= r.RequestHeader[signature_header] then` you might need to change the prefix `sha256=` to something else or remove it completely based on your provider docs ## [Create Function](#create-function) Go to the [Functions](https://my.webhookrelay.com/functions) page and create a new Function with the given code: ``` local crypto = require('crypto') -- Header name that contains HMAC signature local signature_header = 'X-Hub-Signature-256' -- Calculate HMAC of the request body using SHA256 local calculated_hmac, err = crypto.hmac( 'sha256', r.RequestBody, cfg:GetValue("WEBHOOK_SECRET")) if err then error(err) end -- Check whether calculated HMAC matches the one that was sent -- with the message. Adding the prefix "sha256=" as our signature -- that is coming from GitHub will have it. if "sha256=" .. calculated_hmac ~= r.RequestHeader[signature_header] then r:SetResponseStatusCode(401) r:SetResponseBody("Authentication failure: " .. calculated_hmac) r:StopForwarding() return end r:SetResponseBody("OK") ``` Once created, click on "config variables" and add the `WEBHOOK_SECRET` which is used to sign the webhook requests: ![webhook secret setting](https://webhookrelay.com/docs/webhooks/auth/hmac/images/docs/webhooks/auth/webhook-secret.png) and then enter the same secret in the webhook provider. For this example I will be using GitHub: ![github secret setting](https://webhookrelay.com/docs/webhooks/auth/hmac/images/docs/webhooks/auth/github-secret.png) Once webhook is added, GitHub will send a test payload. ## [Testing HMAC verification](#testing-hmac-verification) You can take the payload from GitHub events page and copy paste it into the Function composer together with the `X-Hub-Signature-256` header: ![HMAC verification success](https://webhookrelay.com/docs/webhooks/auth/hmac/images/docs/webhooks/auth/hmac-success.png) ## [Testing HMAC failure case](#testing-hmac-failure-case) HMAC works by computed hash of the body and the secret key. If you edit the request body by just adding some letters and retry, HMAC verification will fail: ![HMAC verification failure](https://webhookrelay.com/docs/webhooks/auth/hmac/images/docs/webhooks/auth/hmac-fail.png) Did this page help you? --- --- title: URL Encoded Form | WebhookRelay meta: "og: title": "URL Encoded Form" description: Parse and convert URL encoded form data into JSON or any other format url: https://webhookrelay.com/docs/webhooks/functions/url-encoded-data.md file: /docs/webhooks/functions/url-encoded-data.md --- ![Stripes](https://webhookrelay.com/docs/webhooks/functions/url-encoded-data/images/stripes.svg) Documentation **Fundamentals** # **URL Encoded Form** Parse and convert URL encoded form data into JSON or any other format Webhook Relay detects application/x-www-form-urlencoded requests and automatically parses them so your function can use it. Parsed form data can be accessed through `r.RequestFormData` / `r.formData` variable. ## [Using decoded values](#using-decoded-values) For example if the payload looks like this: ``` name=john&lastname=wick ``` Then you can access the form elements and use them to create a new payload: ``` const encodedPayload = JSON.stringify({ name: r.formData.name[0], lastname: r.formData.lastname[0] }) r.setHeader("Content-Type", "application/json") r.setBody(encodedPayload) ``` ``` local json = require("json") local encoded_payload = { name= r.RequestFormData.name[1], lastname=r.RequestFormData.lastname[1] } local encoded_payload, err = json.encode(encoded_payload) if err then error(err) end r:SetRequestHeader("Content-Type", "application/json") r:SetRequestBody(encoded_payload) ``` This would transform the webhook into: ``` { "name": "john", "lastname": "wick" } ``` ### [Nested forms](#nested-forms) If your form contains nested fields such as &campaign%5Bid%5D=98 you can access them directly as well: ``` r.setBody(r.formData["campaign[id]"][0]) ``` ``` r:SetRequestBody(r.RequestFormData["campaign[id]"][1]) ``` ## [Prerequisites](#prerequisites) For the decoding to work, Webhook Relay expects a header `Content-Type` that includes `application/x-www-form-urlencoded` and the boundary. Did this page help you? --- --- title: Working with time | WebhookRelay meta: "og: title": "Working with time" description: Webhook Relay provides several helpers when working with time, this section shows how to get current time, how to parse and format time url: https://webhookrelay.com/docs/webhooks/functions/working-with-time.md file: /docs/webhooks/functions/working-with-time.md --- ![Stripes](https://webhookrelay.com/docs/webhooks/functions/working-with-time/images/stripes.svg) Documentation **Fundamentals** # **Working with time** Webhook Relay provides several helpers when working with time, this section shows how to get current time, how to parse and format time Webhook Relay provides several helpers when working with time, this section shows how to get current time, how to parse and format time To get current time in Unix format: ``` const now = time.unix() r.setBody(String(now)) // Will set body to such as '1692450481' ``` ``` local time = require("time") local now = time.unix() r:SetRequestBody(tostring(now)) -- Will set body to such as '1692450481.8626223' ``` ### [Formatting time](#formatting-time) Formatting time with Webhook Relay uses Go-style **reference time** layouts. Whereas other languages use a format like YYYY-MM-DD to format dates like: 2022-10-21, WHR uses a reference time. This reference time is a point in time that the layout uses: **2 January 2006 03:04:05 PM** in the time zone **UTC -7** You can note that numbers follow each other: 1 (January), 2 (day), 3 (hour), 4 (minutes)... ``` // Get current time const now = time.unix() // Declare the layout const layout = "2006-01-02T15:04:05Z07:00" // Format it in our layout const nowHumanReadable = time.format(now, layout) r.setBody(nowHumanReadable) // prints out 2023-08-19T13:21:32Z ``` ``` local time = require("time") -- Get current time local now = time.unix() -- Declare the layout local layout = "2006-01-02T15:04:05Z07:00" -- Format it in our layout local now_human_readable = time.format(now, layout) r:SetRequestBody(now_human_readable) -- prints out 2023-08-19T13:21:32Z ``` More example layouts: ``` Layout = "01/02 03:04:05PM '06 -0700" // The reference time, in numerical order. ANSIC = "Mon Jan _2 15:04:05 2006" UnixDate = "Mon Jan _2 15:04:05 MST 2006" RubyDate = "Mon Jan 02 15:04:05 -0700 2006" RFC822 = "02 Jan 06 15:04 MST" RFC822Z = "02 Jan 06 15:04 -0700" // RFC822 with numeric zone RFC850 = "Monday, 02-Jan-06 15:04:05 MST" RFC1123 = "Mon, 02 Jan 2006 15:04:05 MST" RFC1123Z = "Mon, 02 Jan 2006 15:04:05 -0700" // RFC1123 with numeric zone RFC3339 = "2006-01-02T15:04:05Z07:00" RFC3339Nano = "2006-01-02T15:04:05.999999999Z07:00" Kitchen = "3:04PM" // Handy time stamps. Stamp = "Jan _2 15:04:05" StampMilli = "Jan _2 15:04:05.000" StampMicro = "Jan _2 15:04:05.000000" StampNano = "Jan _2 15:04:05.000000000" ``` ## [Parsing time](#parsing-time) Parsing works the same way as formatting time, you will need to provide a time and the layout. Result will be returned as a Unix timestamp. ``` const ts = time.parse("Dec 2 03:33:05 2018", "Jan 2 15:04:05 2006") ``` ``` time.parse("Dec 2 03:33:05 2018", "Jan 2 15:04:05 2006") ``` ## [Timezones](#timezones) To format the time for some particular timezone, you can pass the third optional parameter into the `time.format()` function, for example: ``` const now = time.unix() const layout = "01.02.2006 15:04" const nowHumanReadable = time.format(now, layout, "CET") r.setBody(nowHumanReadable) ``` ``` local time = require("time") local now = time.unix() local layout = "01.02.2006 15:04" local now_human_readable, err = time.format(now, layout, "CET") if err then error(err) end r:SetRequestBody(now_human_readable) ``` Did this page help you? --- --- title: Read, write request data | WebhookRelay meta: "og: title": "Read, write request data" description: How to access and modify request data in Webhook Relay Functions url: https://webhookrelay.com/docs/webhooks/functions/modify-request.md file: /docs/webhooks/functions/modify-request.md --- ![Stripes](https://webhookrelay.com/docs/webhooks/functions/modify-request/images/stripes.svg) Documentation **Fundamentals** # **Read, write request data** How to access and modify request data in Webhook Relay Functions In this page we will demonstrate basic operations that you can achieve in functions. ## [Accessing request data (incoming webhook/API request)](#accessing-request-data-incoming-webhookapi-request) Functions use the `r` object that provides access to request data (headers, query, method and body) and can then update any HTTP request details. Available data to access: | Lua | JavaScript | Type | Description | | --- | --- | --- | --- | | r.RequestBody | r.body | String | Request body. | | r.RequestMethod | r.method | String | Request method (PUT, POST, DELETE, etc.). | | r.RequestPath | r.path | String | Request path. | | r.RequestRawQuery | r.rawQuery | String | Request query, for example if the request was made with /api?category=electronics, then the query will be _category=electronics_. | | r.RequestHeader | r.headers | Table/Object | A key-value map of headers. | | r.RequestQuery | r.query | Table/Object | A key-value map of query params. | ### [Read request body](#read-request-body) An example of accessing request body and decoding it: ``` // request body is in r.body, use it as any other string: const body = JSON.parse(r.body) ``` ``` -- import json package local json = require("json") -- request body is in r.RequestBody, use it as any other string: local body, err = json.decode(r.RequestBody) if err then error(err) end ``` ### [Reading request headers](#reading-request-headers) To access specific header, use: ``` const myHeader = r.headers["Your-Header-Name"] ``` ``` local my_header = r.RequestHeader["Your-Header-Name"] ``` ### [Reading request URL query](#reading-request-url-query) To read request URL query (for example `/v1/api?hub.mode=subscribe&hub.challenge=1903260781&hub.verify_token=my-token"`) you have two options: 1. `r.RequestQuery["hub.challenge"]` / `r.query["hub.challenge"]` which will return `1903260781` for this example. 2. `r.RequestRawQuery` / `r.rawQuery` which will return full raw query `hub.mode=subscribe&hub.challenge=1903260781&hub.verify_token=my-token"` ## [Modify request data](#modify-request-data) Available methods to update request: | Lua | JavaScript | Parameter Type | Description | | --- | --- | --- | --- | | r:SetRequestBody("string") | r.setBody("string") | String | Update request body | | r:SetRequestMethod("string") | r.setMethod("string") | String | Update request method | | r:SetRequestRawQuery("foo=bar") | r.setRawQuery("foo=bar") | String | Update request raw query | | r:SetRequestPath("/extra/path") | r.setPath("/extra/path") | String | Set additional extra path | | r:SetRequestHeader("key", "value") | r.setHeader("key", "value") | String, String | Set new header key/value pair | An example how to update request object: ``` // set body r.setBody("new body") // set method r.setMethod("POST") // set raw query r.setRawQuery("foo=bar") // set extra path r.setPath("/extra/path") // set header r.setHeader("Content-Type", "application/json") ``` ``` -- set body r:SetRequestBody("new body") -- set method r:SetRequestMethod("string") -- set raw query r:SetRequestRawQuery("foo=bar") -- set extra path r:SetRequestPath("/extra/path") -- set header r:SetRequestHeader("Content-Type", "application/json") ``` ## [Modify response](#modify-response) > Note: customized responses only applicable if function is attached to the **Input** and not bucket's **Output**. Available methods to set customized response: | Lua | JavaScript | Parameter Type | Description | | --- | --- | --- | --- | | r:SetResponseBody("string") | r.setResponseBody("string") | String | Set response body | | r:SetResponseStatusCode(201) | r.setResponseStatus(201) | Integer | Set response status code | | r:SetResponseHeader("key", "value") | r.setResponseHeader("key", "value") | String, String | Set response header key/value pair | ## [Getting configuration values](#getting-configuration-values) Configuration values in functions allow sharing the code without sharing sensitive information or just configuration values that might change between accounts, teams, etc. To add a new configuration value: 1. Create a function 2. Go to function details 3. Click on a tab "CONFIG VARIABLES" 4. Specify config variable key and value. Once you have specified these details, you can start using them in your functions: ``` // configuration is accessible from a global "cfg" variable const projectId = cfg.get("PROJECT_ID") // do something with the value ``` ``` -- configuration is accessible from a global "cfg" variable local project_id = cfg:GetValue("PROJECT_ID") -- do something with the value ``` ## [Filtering requests](#filtering-requests) To filter requests based on body, headers or some external conditions, you can use the stop forwarding method. This will set webhook status to **rejected** and this webhook will not be forwarded: ``` // in this example we will check the payload to make the decision // incoming request body example: // { // "action": "important", // "user": "joe" // } const requestBody = JSON.parse(r.body) // simple comparison of the payload if (requestBody.action === "not_important") { // request is not important, don't forward it r.stopForwarding() return } // Otherwise, continue forwarding or add any additional logic here ``` ``` -- in this example we will check the payload to make the decision local json = require("json") -- incoming request body example: -- { -- "action": "important", -- "user": "joe" -- } local requestBody, err = json.decode(r.RequestBody) if err then error(err) end -- simple comparison of the payload if requestBody["action"] == "not_important" then -- request is not important, don't forward it r:StopForwarding() return end -- Otherwise, continue forwarding or add any additional logic here ``` Did this page help you? --- --- title: Multipart form to JSON | WebhookRelay meta: "og: title": "Multipart form to JSON" description: Parsing multipart form data inside the Webhook Relay Function url: https://webhookrelay.com/docs/webhooks/functions/multipart-form-data.md file: /docs/webhooks/functions/multipart-form-data.md --- ![Stripes](https://webhookrelay.com/docs/webhooks/functions/multipart-form-data/images/stripes.svg) Documentation **Fundamentals** # **Multipart form to JSON** Parsing multipart form data inside the Webhook Relay Function Webhook Relay detects multipart/formdata requests and automatically parses them so your function can use it. Parsed form data can be accessed through `r.RequestFormData` / `r.formData` variable. You can use Webhook Relay to receive a form and convert it into any kind of JSON that can be sent to another API. ## [Using decoded values](#using-decoded-values) For example if the payload fragment looks like this: ``` ... --------------------------5683f7544dff7b07 Content-Disposition: form-data; name="username" John --------------------------5683f7544dff7b07 ... ``` Then to get `username` value (which is `John`) you will need to: ``` // values can be accessed through 'r.formData' object. Since // there can be multiple values for each key, you also need to // specify that it's the first element of the list (0-indexed): const username = r.formData.username[0] const firstName = r.formData.first_name[0] // transforming form data into JSON const jsonPayload = { username: username, first_name: firstName } const encodedPayload = JSON.stringify(jsonPayload) r.setBody(encodedPayload) ``` ``` -- import "json" package when working with JSON local json = require("json") -- values can be accessed through 'r.RequestFormData' object. Since -- there can be multiple values for each key, you also need to -- specify that it's the first element of the list: local username = r.RequestFormData.username[1] local first_name = r.RequestFormData.first_name[1] -- transforming form data into JSON local json_payload = { username = username, first_name = first_name } local encoded_payload, err = json.encode(json_payload) if err then error(err) end r:SetRequestBody(encoded_payload) ``` ## [Prerequisites](#prerequisites) For the decoding to work, Webhook Relay expects a header `Content-Type` that includes `multipart/form-data` and the boundary. Did this page help you? --- --- title: Base64, encryption | WebhookRelay meta: "og: title": "Base64, encryption" description: How to generate hmac, crc32, sha1, sha256, sha512 hashes and encrypt data in Webhook Relay functions url: https://webhookrelay.com/docs/webhooks/functions/crypto-functions.md file: /docs/webhooks/functions/crypto-functions.md --- ![Stripes](https://webhookrelay.com/docs/webhooks/functions/crypto-functions/images/stripes.svg) Documentation **Fundamentals** # **Base64, encryption** How to generate hmac, crc32, sha1, sha256, sha512 hashes and encrypt data in Webhook Relay functions Webhook Relay provides a crypto module to deal with various hashing and cryptography related operations. ## [Encoding and decoding base64 data](#encoding-and-decoding-base64-data) To base64 encode a value: ``` const encoded = crypto.base64Encode("some value") // do something with encoded value ``` ``` -- importing the package local crypto = require('crypto') local encoded = crypto.base64_encode("some value") -- do something with encoded value ``` To decode some value: ``` // supplying base64 value const decoded = crypto.base64Decode("aGVsbG8gd29ybGQ=") // do something with 'decoded' value ``` ``` -- importing the package local crypto = require('crypto') -- supplying base64 value local decoded, err = crypto.base64_decode("aGVsbG8gd29ybGQ=") if err then error(err) end -- do something with 'decoded' value ``` ## [Create MD5 hash](#create-md5-hash) To create [MD5 message-digest algorithm](https://en.wikipedia.org/wiki/MD5) based hashes: ``` const md5HashedValue = crypto.md5("") ``` ``` -- importing the package local crypto = require('crypto') local md5_hashed_value = crypto.md5("") ``` > Note: MD5 is considered cryptographically broken, if you can, use SHA256 hashing algorithm. ## [Create SHA1, SHA256, SHA512 hashes](#create-sha1-sha256-sha512-hashes) [SHA-2 (Secure Hash Algorithm 2)](https://en.wikipedia.org/wiki/SHA-2) hashing functions are provided by the `crypto` module: ``` // to hash with SHA1 const sha1HashedValue = crypto.sha1("") // to hash with SHA256 const sha256HashedValue = crypto.sha256("") // to hash with SHA512 const sha512HashedValue = crypto.sha512("") ``` ``` -- importing the package local crypto = require('crypto') -- to hash with SHA1 local sha1_hashed_value = crypto.sha1("") -- to hash with SHA256 local sha256_hashed_value = crypto.sha256("") -- to hash with SHA512 local sha512_hashed_value = crypto.sha512("") ``` ## [Calculating HMAC](#calculating-hmac) [HMAC](https://en.wikipedia.org/wiki/HMAC) can be calculated using MD5, SHA1, SHA256 and SHA512 algorithms combined with the data and the key. It may be used to simultaneously verify both the data integrity and the authenticity of a message. ``` // to calculate HMAC of the request body using SHA256: // note: parameter order is (algorithm, key, message) const calculatedHmac = crypto.hmac("sha256", "", r.body) // check whether calculated HMAC matches the one that was sent // with the message ``` ``` -- importing the package local crypto = require('crypto') -- to calculate HMAC of the request body using SHA256: -- note: parameter order is (algorithm, data, key) local calculated_hmac = crypto.hmac('sha256', r.RequestBody , '') -- check whether calculated HMAC matches the one that was sent -- with the message ``` ## [Calculating CRC32 checksum](#calculating-crc32-checksum) [CRC32](https://en.wikipedia.org/wiki/Cyclic_redundancy_check) is an error-detecting code commonly used to detect accidental changes to raw data. ``` // to get a string value of crc hash: const encodedCrc = crypto.crc32(r.body) ``` ``` -- importing the package local crypto = require('crypto') -- to get a string value of crc hash (uses hex encoding): local encoded_crc2 = crypto.crc32(r.RequestBody) -- to get a raw value: local raw_crc2 = crypto.crc32(r.RequestBody, true) ``` Did this page help you? --- --- title: Sending emails | WebhookRelay meta: "og: title": "Sending emails" description: Webhook Relay provides a Mailgun package to easily send emails on various events. url: https://webhookrelay.com/docs/webhooks/functions/send-emails.md file: /docs/webhooks/functions/send-emails.md --- ![Stripes](https://webhookrelay.com/docs/webhooks/functions/send-emails/images/stripes.svg) Documentation **Fundamentals** # **Sending emails** Webhook Relay provides a Mailgun package to easily send emails on various events. Webhook Relay provides a Mailgun module to easily send emails on various events. ## [Prerequisites](#prerequisites) - [Mailgun](https://www.mailgun.com/) account - [Webhook Relay](https://my.webhookrelay.com/) account ## [Create a Function](#create-a-function) First, head to the [Functions page](https://my.webhookrelay.com/functions) and create a new function called `mailgun`. You can use the **mailgun** module to send emails. To start sending emails, create a new Function. This function will need an API key and domain: ``` const domain = cfg.get("domain") const apiKey = cfg.get("api_key") // mailgun.initialize('domain', 'api-key', 'region (us/eu)') const initResult = mailgun.initialize(domain, apiKey, "us") if (initResult && initResult.error) { console.error("Mailgun init failed:", initResult.error) } // mailgun.send('sender@foo.com', 'subject', 'body-here', 'recipient@foo.com') const sendResult = mailgun.send("your-email@example.com", "test subject", "test body", "john@example.com") if (sendResult && sendResult.error) { console.error("Email send failed:", sendResult.error) } ``` ``` -- Import Mailgun helper package local mailgun = require('mailgun') local domain = cfg:GetValue('domain') local api_key = cfg:GetValue('api_key') -- mailgun.initialize('domain', 'api-key', 'region (us/eu)') err = mailgun.initialize(domain, api_key, 'us') if err then error(err) end -- mailgun.send('sender@foo.com', 'subject', 'body-here', 'recipient@foo.com') err = mailgun.send('your-email@example.com', 'test subject', 'test body', 'john@example.com') if err then error(err) end ``` Then, you will need to enter your API key from the [API keys page](https://app.mailgun.com/app/account/security/api_keys) and set the as config variables for your function. You can find more details on how to find your API keys in [Mailgun](https://help.mailgun.com/hc/en-us/articles/203380100-Where-Can-I-Find-My-API-Key-and-SMTP-Credentials-) here. ## [Create a Bucket](#create-a-bucket) Then, in the [https://my.webhookrelay.com/buckets](https://my.webhookrelay.com/buckets) click on "create empty bucket" and type `webhook-to-mail`. Once created, go to the input details and click on "transform" section to select your Function: ![selecting image](https://webhookrelay.com/docs/webhooks/functions/send-emails/images/docs/webhooks/functions/function-select.png) ## [Receiving webhooks](#receiving-webhooks) In your input or bucket details page you should see your public endpoint, use it to receive webhooks. Did this page help you? --- --- title: Integrating into CI/CD | WebhookRelay meta: "og: title": "Integrating into CI/CD" description: A guide on how to automatically deploy Functions in various source control management systems url: https://webhookrelay.com/docs/webhooks/functions/integrate-into-cicd.md file: /docs/webhooks/functions/integrate-into-cicd.md --- ![Stripes](https://webhookrelay.com/docs/webhooks/functions/integrate-into-cicd/images/stripes.svg) Documentation **Fundamentals** # **Integrating into CI/CD** A guide on how to automatically deploy Functions in various source control management systems While it's easy to start using Functions straight from web dashboard, it's a good practice to keep the source in source control management (SCM), also known as [version control](https://en.wikipedia.org/wiki/Version_control) systems such as: - [Github](https://github.com/) - [Bitbucket](https://bitbucket.org) - [Gitlab](https://about.gitlab.com/) ## [Bitbucket pipelines](#bitbucket-pipelines) Bitbucket function example can be found here: [https://bitbucket.org/rusenas/webhookrelay-function-example/src/master/](https://bitbucket.org/rusenas/webhookrelay-function-example/src/master/). Updates are done using [Bitbucket pipelines](https://bitbucket.org/product/features/pipelines) and the official [Webhook Relay pipe](https://bitbucket.org/product/features/pipelines/integrations?p=webhookrelay/webhookrelay-function-deploy) which deploys your code. ### [Setup](#setup) 1. Create a new function here [https://my.webhookrelay.com/functions](https://my.webhookrelay.com/functions) (or using `relay` CLI) 2. Get your access token key & secret [https://my.webhookrelay.com/tokens](https://my.webhookrelay.com/tokens) 3. Configure Bitbucket repository settings with access token: - Go to "Repository settings" - Click on "Repository variables" (if pipelines are not enabled, enable them) - Add two environment variables RELAY_KEY (with value from the access token 'key') and RELAY_SECRET (with value from the access token 'secret') 4. Create `bitbucket-pipelines.yml` file in your repository root directory: ``` image: golang:1.12 pipelines: default: - step: script: - pipe: webhookrelay/webhookrelay-function-deploy:0.2.4 variables: FUNCTION_NAME: 'name of your functions' # Replace with your function name FUNCTION_FILE: 'function_file_name.lua' # Replace with your function filename RELAY_KEY: $RELAY_KEY RELAY_SECRET: $RELAY_SECRET ``` That's it, you can now push your functions and get them updated. Did this page help you? --- --- title: GCP BigQuery | WebhookRelay meta: "og: title": "GCP BigQuery" description: How to send data to BigQuery from Webhook Relay. url: https://webhookrelay.com/docs/webhooks/functions/big-query.md file: /docs/webhooks/functions/big-query.md --- ![Stripes](https://webhookrelay.com/docs/webhooks/functions/big-query/images/stripes.svg) Documentation **Fundamentals** # **GCP BigQuery** How to send data to BigQuery from Webhook Relay. Prerequisites: - [Google Cloud Platform account](https://cloud.google.com/) (free trial available) - Google Cloud project with BigQuery enabled (there's a generous free tier available for BigQuery) - Dataset and table in BigQuery - [https://cloud.google.com/bigquery/docs/tables](https://cloud.google.com/bigquery/docs/tables) Webhook Relay provides a bigquery module that can stream writes into Google Cloud BigQuery. To start ingesting data from webhooks straight into your BigQuery table, create a new Function and import the bigquery module: ``` // BigQuery module is available globally // Initialize with your project details bigquery.initialize("your-project-id", "dataset-id", "table-id") ``` ``` -- Import BigQuery helper package local bigquery = require('bigquery') ``` A new tab should appear that will ask you to set up credentials: Go to that tab and it will ask you to: 1. Create [new service accounts](https://cloud.google.com/iam/docs/service-accounts) with **BigQuery Editor** permissions 2. Download the JSON file. Once you have the JSON file 3. Copy & paste contents into the form and click save. ## [Streaming data into BigQuery](#streaming-data-into-bigquery) ``` // Parsing payload const rowData = JSON.parse(r.body) // Initializing BigQuery client const initResult = bigquery.initialize("your-project-id", "dataset-id", "table-id") if (initResult && initResult.error) { console.error("BigQuery init failed:", initResult.error) } // Receiving payload: // { // "hub_user_id": "user-id-here", // "category": "signup", // "action": "click", // "label": "github auth" // } // Insert row: const insertResult = bigquery.insert(rowData) if (insertResult && insertResult.error) { console.error("Insert failed:", insertResult.error) } ``` ``` -- Import BigQuery helper package local bigquery = require('bigquery') local json = require("json") -- Parsing payload local rowData, err = json.decode(r.RequestBody) if err then error(err) end -- Initializing BigQuery client err = bigquery.initialize('your-project-id', 'dataset-id', 'table-id') if err then error(err) end -- Receiving payload: -- { -- "hub_user_id": "user-id-here", -- "category": "signup", -- "action": "click", -- "label": "github auth" -- } -- Insert row: err = bigquery.insert(rowData) if err then error(err) end ``` ## [Check if record exists](#check-if-record-exists) A simple query to check whether a row exists by matching a column with a value: ``` bigquery.initialize("your-project-id", "dataset-id", "table-id") const result = bigquery.recordExists("name", "john") if (result.error) { console.error("Query failed:", result.error) } if (result.exists) { // OK } else { console.error("Record not found") } ``` ``` bigquery = require('bigquery') err = bigquery.initialize('your-project-id', 'dataset-id', 'table-id') if err then error(err) end local exists, err = bigquery.record_exists('name', 'john') if err then error(err) end if exists then -- OK else error('Record not found') end ``` Use cases: - You are working with a webhook that sends data about a user signing up. You want to check if the user already exists in your database before inserting a new row. - If each inserted unique webhook results in an expensive operation you want to avoid running the operation if the row already exists. ## [Execute any command](#execute-any-command) To execute any SQL command on your table: ``` bigquery.initialize("your-project-id", "dataset-id", "table-id") // Delete old records of the matching category. Method 'exec' can take an arbitrary // number of arguments, depending on how many ? you have in your query. const result = bigquery.exec("DELETE dataset-id.table-id WHERE category = ? AND country = ?", "movies", "US") if (result && result.error) { console.error("Query failed:", result.error) } ``` ``` bigquery = require('bigquery') err = bigquery.initialize('your-project-id', 'dataset-id', 'table-id') if err then error(err) end -- Delete old records of the matching category. Method 'exec' can take an arbitrary -- number of arguments, depending on how many ? you have in your query. err = bigquery.exec('DELETE dataset-id.table-id WHERE category = ? AND country = ?', 'movies', 'US') if err then error(err) end ``` ## [BigQuery module API reference](#bigquery-module-api-reference) | Lua method | JavaScript method | Parameter Type | Description | | --- | --- | --- | --- | | insert(rowData) | insert(rowData) | Table/Object | A key-value object that represents a row data. | | record_exists(column, value) | recordExists(column, value) | String, String | Checks if a row with the matching column exists | | exec(query, ...args) | exec(query, ...args) | String, ...any | Execute a DML query with positional parameters | ## [Limitations](#limitations) Currently the module doesn't support nested objects. That means that a table with a JSON structure such as: ``` { "hub_user_id": "user-id-here", "category": "signup", "action": "click", "label": "github auth", "nested_data": { "location": "GB", "date": "2020-05-10" } } ``` will not be successfully inserted. Therefore, flatten the structure in the function before inserting it. ## [Troubleshooting](#troubleshooting) Few things to note: - Ensure that project ID, dataset ID and table ID are there. - BigQuery table schema is defined by the user. You don't have to write all the fields (most of the can be nullable) but if you try to write a field that doesn't exist, BigQuery will refuse to write. Did this page help you? --- --- title: Accessing metadata | WebhookRelay meta: "og: title": "Accessing metadata" description: Accessing metadata from Webhook Relay Functions url: https://webhookrelay.com/docs/webhooks/functions/accessing-metadata.md file: /docs/webhooks/functions/accessing-metadata.md --- ![Stripes](https://webhookrelay.com/docs/webhooks/functions/accessing-metadata/images/stripes.svg) Documentation **Fundamentals** # **Accessing metadata** Accessing metadata from Webhook Relay Functions It can be useful to access metadata from the function. For example, you can access the bucket, input and output names or the IDs directly from the function. This way you can build more complex functions that can be used in different scenarios. ## [Accessing metadata](#accessing-metadata) You can access the metadata from the function: ``` r.metadata["bucket_name"] ``` ``` r.Metadata["bucket_name"] ``` ## [Available metadata](#available-metadata) | Metadata | Description | | --- | --- | | bucket_id | The ID of the bucket | | bucket_name | The name of the bucket | | input_id | The ID of the input | | input_name | The name of the input | | output_id | The ID of the output | | output_name | The name of the output | | output_url | The URL of the output | ## [Example function deciding based on bucket name](#example-function-deciding-based-on-bucket-name) Here's an example accessing Bucket name from within the function: ``` const bucketName = r.metadata["bucket_name"] if (bucketName === "first-bucket") { // do something const response = http.request("GET", "https://company-a.com") // exit from function return } if (bucketName === "second-bucket") { // do something const response = http.request("GET", "https://company-b.com") // exit from function return } ``` ``` local http = require("http") local bucket_name = r.Metadata["bucket_name"] if bucket_name == "first-bucket" then -- do something response, err = http.request("GET", "https://company-a.com") if err then error(err) end -- exit from function return end if bucket_name == "second-bucket" then -- do something response, err = http.request("GET", "https://company-b.com") if err then error(err) end -- exit from function return end ``` Did this page help you? --- --- title: Response (post-delivery) functions | WebhookRelay meta: "og: title": "Response (post-delivery) functions" description: Run a JavaScript or Lua function after a webhook is delivered to alert on failures, push metrics, or flag bad responses. url: https://webhookrelay.com/docs/webhooks/functions/response-functions.md file: /docs/webhooks/functions/response-functions.md --- ![Stripes](https://webhookrelay.com/docs/webhooks/functions/response-functions/images/stripes.svg) Documentation **Fundamentals** # **Response (post-delivery) functions** Run a JavaScript or Lua function after a webhook is delivered to alert on failures, push metrics, or flag bad responses. A **response function** is a JavaScript or Lua function attached to an output that runs **after** a webhook has been delivered — once the destination's response, or the final delivery error after all retries are exhausted, is known. It is the counterpart to the regular **request function** (also called a transform function), which runs _before_ delivery and reshapes the outgoing request. A response function runs at the other end of the pipeline, on the delivery **outcome**. Use it to add custom logic when a delivery succeeds or fails: - **Alert on failures** — send a Slack message or email when a delivery returns a 5xx, times out, hits a connection error, or returns an unexpected status. - **Catch silent failures** — treat a `200` with an empty body (or a `{"ok": false}` payload) as a failure. - **Push metrics or open tickets** — react to the outcome of every delivery. - **Flag the recorded response** — override the status/body/headers saved to the webhook log so the dashboard and failure alerts reflect what really happened. > A response function **cannot change what was already returned to the sender** — the webhook is already delivered. It _can_ rewrite the response that gets **recorded** to the log (see [Rewriting the recorded response](#rewriting-the-recorded-response)). ## [Configuring](#configuring) A response function is an ordinary function — create it the same way as any other, but select the **RESPONSE** tab in the editor. That tab exposes the response examples (alert on failure, mark delivery failed, empty/unexpected response) and the delivery-outcome fields described below. ![Creating a response function from the RESPONSE tab, with ready-made example templates](https://webhookrelay.com/docs/webhooks/functions/response-functions/images/docs/webhooks/functions/create_response_function.png) An output then has two independent function slots: | Slot | Field | When it runs | | --- | --- | --- | | Request function | `function_id` | before delivery — can transform the outgoing request | | Response function | `response_function_id` | after delivery — reads the outcome, can flag the recorded response | Both are optional. In the dashboard, open the output and use the **Functions** row — a dialog with two dropdowns, clearly marked "runs before delivery" and "runs after delivery". Pick your response function from the second dropdown. ![Attaching a response function to an output — the Functions dialog has separate request and response dropdowns](https://webhookrelay.com/docs/webhooks/functions/response-functions/images/docs/webhooks/functions/select_response_function.png) You can also set them over the API: ``` PUT /v1/buckets/{bucketId}/outputs/{outputId} { "name": "my-endpoint", "destination": "https://api.example.com/hook", "function_id": "", // runs before delivery (optional) "response_function_id": "" // runs after delivery (optional) } ``` The referenced function must belong to your account; it is validated on create/update exactly like `function_id`. The `create_output` / `update_output` MCP tools accept `response_function_id` too. ## [The `r` object](#the-r-object) In addition to the usual request fields (`r.body`, `r.headers`, `r.method`, `r.metadata`, …), a response function reads the delivery outcome: | JavaScript | Lua | Description | | --- | --- | --- | | `r.responseStatus` | `r.ResponseStatus` | HTTP status code. **`0` means no HTTP response** — a connection error, timeout, or TLS failure. | | `r.responseBody` | `r.ResponseBody` | Response body (string). | | `r.responseHeaders` | `r.ResponseHeader` | Response headers (object/table). | | `r.error` | `r.DeliveryError` | Transport error string when the status is `0`; empty when an HTTP response was received (even a 5xx). | `r.metadata` carries `output_name`, `output_url`, `bucket_name`, `bucket_id`, `output_id` (and `input_name` / `input_id` when available), so an alert can name the output that failed. See [Accessing metadata](https://webhookrelay.com/docs/webhooks/functions/response-functions/docs/webhooks/functions/accessing-metadata/). The function always runs for **every** terminal outcome — success and failure — so the function itself decides what to act on (for example, only alert when `responseStatus >= 400`). ## [Example: alert on a failed delivery](#example-alert-on-a-failed-delivery) This is the simplest, alert-only pattern: when a delivery fails (no response, or a 4xx/5xx), post a message to Slack. Set `slack_webhook_url` as a [config variable](https://webhookrelay.com/docs/webhooks/functions/response-functions/docs/webhooks/functions/modify-request/#getting-configuration-values) on the function. ``` // Runs AFTER the webhook is delivered. Read-only alert on failure. var failed = r.responseStatus === 0 || r.responseStatus >= 400; if (failed) { var reason = r.responseStatus === 0 ? ("delivery error: " + r.error) // 0 == timeout / connection / TLS error : ("HTTP " + r.responseStatus); console.log("delivery to " + r.metadata["output_name"] + " failed: " + reason); var url = cfg.get("slack_webhook_url"); if (url) { http.post(url, JSON.stringify({ text: "⚠️ Webhook delivery to *" + r.metadata["output_name"] + "* failed: " + reason }), { headers: { "Content-Type": "application/json" } }); } } ``` ``` -- Runs AFTER the webhook is delivered. Read-only alert on failure. local http = require("http") local json = require("json") local failed = r.ResponseStatus == 0 or r.ResponseStatus >= 400 if failed then local reason = (r.ResponseStatus == 0) and ("delivery error: " .. r.DeliveryError) -- 0 == timeout / connection / TLS error or ("HTTP " .. r.ResponseStatus) local out_name = r.metadata["output_name"] or "output" local payload, err = json.encode({ text = "Webhook delivery to " .. out_name .. " failed: " .. reason }) if err then error(err) end local url = cfg:GetValue("slack_webhook_url") if url ~= "" then http.request("POST", url, { headers = { ["Content-Type"] = "application/json" }, body = payload }) end end ``` ## [Example: treat an empty 200 as a failure](#example-treat-an-empty-200-as-a-failure) Some endpoints answer `200 OK` but silently drop the payload. This catches that case in addition to ordinary 4xx/5xx/timeout failures, then alerts with structured detail. Set `alert_webhook_url` as a config variable. ``` var hardFailure = r.responseStatus === 0 || r.responseStatus >= 400; var emptyOk = r.responseStatus >= 200 && r.responseStatus < 300 && (!r.responseBody || r.responseBody.trim() === ""); if (hardFailure || emptyOk) { var detail = hardFailure ? (r.responseStatus === 0 ? r.error : ("status " + r.responseStatus)) : "200 with an empty body"; console.error("suspicious delivery to " + r.metadata["output_name"] + ": " + detail); var url = cfg.get("alert_webhook_url"); if (url) { http.post(url, JSON.stringify({ output: r.metadata["output_name"], destination: r.metadata["output_url"], status: r.responseStatus, detail: detail }), { headers: { "Content-Type": "application/json" } }); } } ``` ``` local http = require("http") local json = require("json") local hard_failure = r.ResponseStatus == 0 or r.ResponseStatus >= 400 local empty_ok = r.ResponseStatus >= 200 and r.ResponseStatus < 300 and (r.ResponseBody == nil or r.ResponseBody == "") if hard_failure or empty_ok then local detail = hard_failure and (r.ResponseStatus == 0 and r.DeliveryError or ("status " .. r.ResponseStatus)) or "200 with an empty body" local payload, err = json.encode({ output = r.metadata["output_name"] or "output", destination = r.metadata["output_url"] or "", status = r.ResponseStatus, detail = detail }) if err then error(err) end local url = cfg:GetValue("alert_webhook_url") if url ~= "" then http.request("POST", url, { headers = { ["Content-Type"] = "application/json" }, body = payload }) end end ``` ## [Rewriting the recorded response](#rewriting-the-recorded-response) A response function can override the status code, body, and headers that get **recorded** to the webhook log. This does **not** change what was already returned to the sender — the webhook is already delivered — but the recorded delivery status is recomputed from the (possibly overridden) status code. Use it to mark a delivery failed when the destination answered `2xx` but the payload says otherwise, so the log and dashboard show it as failed and your failure alerts fire. It uses the same mutators the request/transform functions use: | JavaScript | Lua | Effect | | --- | --- | --- | | `r.setResponseStatus(code)` | `r:SetResponseStatusCode(code)` | overrides the recorded status code (a 5xx marks the delivery failed) | | `r.setResponseBody(s)` | `r:SetResponseBody(s)` | overrides the recorded response body | | `r.setResponseHeader(k, v)` | `r:SetResponseHeader(k, v)` | overrides/adds a recorded response header | Only the fields you set change; the rest of the destination's response is preserved. This example flags a `200`-with-empty-body delivery (and, in JS, a `{"ok": false}` payload) as a `502` failure: ``` var ok2xx = r.responseStatus >= 200 && r.responseStatus < 300; var emptyBody = !r.responseBody || r.responseBody.trim() === ""; // Also catch endpoints that always answer 200 but embed {"ok": false}. var appError = false; if (ok2xx && !emptyBody) { try { var parsed = JSON.parse(r.responseBody); appError = parsed && parsed.ok === false; } catch (e) { // not JSON — leave appError false } } if (ok2xx && (emptyBody || appError)) { // Override the recorded response: mark it failed. Only the fields we set // change; the rest of the response is preserved. r.setResponseStatus(502); r.setResponseHeader("X-Webhookrelay-Flagged", "response-function"); if (emptyBody) { r.setResponseBody("empty body from destination (flagged by response function)"); } } ``` ``` local ok2xx = r.ResponseStatus >= 200 and r.ResponseStatus < 300 local empty_body = r.ResponseBody == nil or r.ResponseBody == "" if ok2xx and empty_body then r:SetResponseStatusCode(502) r:SetResponseHeader("X-Webhookrelay-Flagged", "response-function") r:SetResponseBody("empty body from destination (flagged by response function)") end ``` ## [When it runs (exactly once)](#when-it-runs-exactly-once) A response function runs **once** per delivery, on the terminal outcome, regardless of which delivery path the webhook takes: | Output / path | When it runs | | --- | --- | | Public HTTP, no durability | on the terminal outcome (sent/failed) | | Public HTTP, durable retry | when durable retry reaches a terminal outcome — success, terminal 4xx, or give-up after the deadline | | Throttled output | on the terminal outcome (a failure handed to durable retry fires when retries finish) | | Internal (relay agent) output | when the agent reports the delivery result back | | Manual retry / redeliver | on the re-delivery's terminal outcome | A webhook handed off to durable retry is recorded as `stalled` (not terminal) on the immediate path, so the function does **not** fire early — it runs once durable retry finishes. > **Internal (relay-agent) outputs** run the response function on the gRPC agent path, where the agent reports its delivery result back. The legacy websocket agent stream is fire-and-forget (it carries no response), so for full-fidelity response functions on internal outputs, use a gRPC-based relay agent. ## [Observability](#observability) - Each run increments `relay_response_function_runs_total{result="ok|function_error|exec_error"}`. - Reactor execute logs (under the function's **Logs**) capture every run, attributed to the `response` step. The webhook log stores the run's execution id, so the log-details view renders a **Response function diff** showing what the function recorded. - A function that throws emits a `function error` notification with `step = "response"`. ## [Next steps](#next-steps) - [Alerting from functions](https://webhookrelay.com/docs/webhooks/functions/response-functions/docs/webhooks/functions/alerting/) — short recipes: alert when the request is not valid JSON, or when the response body is empty. - [Read, write request & response data](https://webhookrelay.com/docs/webhooks/functions/response-functions/docs/webhooks/functions/modify-request/) — the request-side mutators and config variables. - [Make HTTP requests](https://webhookrelay.com/docs/webhooks/functions/response-functions/docs/webhooks/functions/make-http-request/) — call external APIs (used here to send alerts). - [Accessing metadata](https://webhookrelay.com/docs/webhooks/functions/response-functions/docs/webhooks/functions/accessing-metadata/) — `output_name`, `output_url`, and the rest of `r.metadata`. - [Send emails](https://webhookrelay.com/docs/webhooks/functions/response-functions/docs/webhooks/functions/send-emails/) — alert by email instead of an HTTP call. Did this page help you? --- --- title: Custom webhook subdomains | WebhookRelay meta: "og: title": "Custom webhook subdomains" description: Receive, process and forward webhooks using webhookrelay.com subdomain. url: https://webhookrelay.com/docs/webhooks/custom-subdomains.md file: /docs/webhooks/custom-subdomains.md --- ![Stripes](https://webhookrelay.com/docs/webhooks/custom-subdomains/images/stripes.svg) Documentation **Fundamentals** # **Custom webhook subdomains** Receive, process and forward webhooks using webhookrelay.com subdomain. **Looking for using your own domain instead of subdomain? Check out **[Custom Domains](https://webhookrelay.com/docs/webhooks/custom-subdomains/docs/webhooks/custom-domains/)** guide.** ## [Step 1: Claim your subdomain in Webhook Relay](#step-1-claim-your-subdomain-in-webhook-relay) First, navigate to [Webhook Relay domains page](https://my.webhookrelay.com/domains/) and click on "Claim subdomain" button. ![Custom Subdomains](https://webhookrelay.com/docs/webhooks/custom-subdomains/images/features/custom-subdomains/cover.png) You can choose any subdomain you want (as long as it's not already taken), for example `my-app.hooks.webhookrelay.com`, `acme.hooks.webhookrelay.com` or `events-acme.webhookrelay.com`, etc. ## [Step 2: Select your subdomain in the Webhook Relay bucket](#step-2-select-your-subdomain-in-the-webhook-relay-bucket) Last step is to select your domain in the Webhook Relay bucket's input settings: ![Select domain](https://webhookrelay.com/docs/webhooks/custom-subdomains/images/features/custom-domains/select-domain.png) Path is optional but it allows you to reuse same domain for multiple buckets and inputs by using different paths. ## [Benefits of using custom subdomains](#benefits-of-using-custom-subdomains) - **No DNS configuration required:** You don't need to configure any DNS records to use your custom subdomain. - **Stable & Reusable URLs:** Just like custom subdomains, your custom domain URL remains constant. You can point it to different buckets within Webhook Relay as your needs evolve, without requiring changes in your webhook provider configurations. Did this page help you? --- --- title: Functions | WebhookRelay meta: "og: title": Functions description: Use functions to transform webhooks, modify payloads, filter requests, integrate systems, and more. url: https://webhookrelay.com/docs/webhooks/functions.md file: /docs/webhooks/functions.md --- ![Stripes](https://webhookrelay.com/docs/webhooks/functions/images/stripes.svg) Documentation **Fundamentals** # **Functions** Use functions to transform webhooks, modify payloads, filter requests, integrate systems, and more. Functions let you run custom code on every webhook that passes through Webhook Relay. Use them to transform payloads, modify headers, filter unwanted requests, make HTTP calls to external APIs, and integrate different systems together. Functions can be written in **JavaScript** or **Lua**. Both languages have access to the same request object (`r`) and built-in modules for HTTP requests, JSON, cryptography, and more. ## [What can you do with functions?](#what-can-you-do-with-functions) - **Transform payloads** — reshape webhook data from one format to another, for example converting a GitHub push event into a Slack message. - **Filter requests** — inspect incoming webhooks and reject ones that don't match your criteria. - **Modify headers and method** — add authentication headers, change the HTTP method, or set a custom path before forwarding. - **Make HTTP requests** — call external APIs to enrich data, fetch tokens, or send notifications to multiple services. - **Alert on bad requests or responses** — post to Slack, Discord, or any URL when JSON is missing or a destination returns an empty body. See [Alerting from functions](https://webhookrelay.com/docs/webhooks/functions/docs/webhooks/functions/alerting/). - **Validate webhooks** — verify HMAC signatures, check shared secrets, and ensure webhook authenticity. - **Customize responses** — return custom status codes and response bodies to the webhook sender. ## [Quick example: transform a payload](#quick-example-transform-a-payload) This function takes an incoming JSON webhook and reshapes it into a Slack message format: ``` const payload = JSON.parse(r.body) const slackMessage = { text: \`New event from ${payload.source}: ${payload.message}\` } r.setBody(JSON.stringify(slackMessage)) r.setHeader("Content-Type", "application/json") ``` ``` local json = require("json") local payload, err = json.decode(r.RequestBody) if err then error(err) end local slack_message = { text = "New event from " .. payload.source .. ": " .. payload.message } local encoded, err = json.encode(slack_message) if err then error(err) end r:SetRequestBody(encoded) r:SetRequestHeader("Content-Type", "application/json") ``` ## [Quick example: filter requests](#quick-example-filter-requests) This function only forwards webhooks that have an `action` field set to `"completed"`: ``` const payload = JSON.parse(r.body) if (payload.action !== "completed") { r.stopForwarding() return } ``` ``` local json = require("json") local payload, err = json.decode(r.RequestBody) if err then error(err) end if payload.action ~= "completed" then r:StopForwarding() return end ``` ## [Quick example: call an external API](#quick-example-call-an-external-api) Functions can make HTTP requests to enrich webhook data or notify other services: ``` const payload = JSON.parse(r.body) // look up additional data from an external API const resp = http.request("GET", "https://api.example.com/users/" + payload.user_id, { headers: { Authorization: "Bearer " + cfg.get("API_TOKEN") } }) const user = JSON.parse(resp.body) // enrich the original payload with user data payload.user_name = user.name payload.user_email = user.email r.setBody(JSON.stringify(payload)) ``` ``` local json = require("json") local http = require("http") local payload, err = json.decode(r.RequestBody) if err then error(err) end -- look up additional data from an external API local resp, err = http.request("GET", "https://api.example.com/users/" .. payload.user_id, { headers = { Authorization = "Bearer " .. cfg:GetValue("API_TOKEN") } }) if err then error(err) end local user, err = json.decode(resp.body) if err then error(err) end -- enrich the original payload with user data payload.user_name = user.name payload.user_email = user.email local encoded, err = json.encode(payload) if err then error(err) end r:SetRequestBody(encoded) ``` ## [Next steps](#next-steps) Explore the guides in this section for detailed examples: - [JSON encoding](https://webhookrelay.com/docs/webhooks/functions/docs/webhooks/functions/manipulating-json/) — parse and construct JSON payloads. - [Make HTTP requests](https://webhookrelay.com/docs/webhooks/functions/docs/webhooks/functions/make-http-request/) — call external APIs from your functions. - [Read, write request data](https://webhookrelay.com/docs/webhooks/functions/docs/webhooks/functions/modify-request/) — access and modify headers, body, method, and query. - [Base64, encryption](https://webhookrelay.com/docs/webhooks/functions/docs/webhooks/functions/crypto-functions/) — hash, sign, and encode data. - [Working with time](https://webhookrelay.com/docs/webhooks/functions/docs/webhooks/functions/working-with-time/) — parse and format timestamps. - [Send emails](https://webhookrelay.com/docs/webhooks/functions/docs/webhooks/functions/send-emails/) — send email notifications from functions. - [Response (post-delivery) functions](https://webhookrelay.com/docs/webhooks/functions/docs/webhooks/functions/response-functions/) — run code on the delivery outcome to alert on failures or flag bad responses. Did this page help you? --- --- title: Schedule recurring webhooks | WebhookRelay meta: "og: title": "Schedule recurring webhooks" description: Schedule recurring webhooks with Webhook Relay url: https://webhookrelay.com/docs/webhooks/cron/using-cron-webhooks.md file: /docs/webhooks/cron/using-cron-webhooks.md --- ![Stripes](https://webhookrelay.com/docs/webhooks/cron/using-cron-webhooks/images/stripes.svg) Documentation **Fundamentals** # **Schedule recurring webhooks** Schedule recurring webhooks with Webhook Relay In this tutorial, we will learn how to schedule recurring webhooks with Webhook Relay. But first, let's understand some key concepts: - A webhook is a way for one application to send real-time information to another application. - Recurring webhooks are webhooks that are sent automatically at scheduled intervals. - Webhook Relay is a service that helps manage and deliver webhooks reliably. You can use recurring webhooks to trigger various events in other services, such as starting workflows in Zapier, Make.com, or IFTTT. This is useful for automating tasks that need to happen regularly, like daily data updates or weekly report generation. Let's get started! ## [Set up a receiving inbox](#set-up-a-receiving-inbox) Go to the [Webhook Bin](https://bin.webhookrelay.com/#/bins/7c8aa24d-f32e-4e9c-a175-68caf7029c0c) to get a new inbox URL, we will use this to test our cron webhooks. ## [Configure cron webhooks](#configure-cron-webhooks) Now, head to [cron configuration page](https://my.webhookrelay.com/cron) and configure a new webhook: ![Cron configuration](https://webhookrelay.com/docs/webhooks/cron/using-cron-webhooks/images/docs/webhooks/cron/configure.png) Set the webhook URL to the inbox URL we got from the webhook bin, click "Every minute", set request body to anything you want and click "Create". ## [Advanced time configuration](#advanced-time-configuration) You can also generate a custom schedule using cron expression. Just click "Generate Cron Expression" and you will see a form like this: ![Custom generator](https://webhookrelay.com/docs/webhooks/cron/using-cron-webhooks/images/docs/webhooks/cron/custom.png) You can use this expression to schedule your webhook to run every day at 10 AM on specific days or months, etc. ## [Test your cron webhook](#test-your-cron-webhook) Now, wait for a minute and you should see the webhook request in the Webhook Bin. It will contain the request body and headers. You can view all outgoing requests in Webhook Relay dashboard too, just click "logs" next to your cron configuration: ![Webhooks logs for cron](https://webhookrelay.com/docs/webhooks/cron/using-cron-webhooks/images/docs/webhooks/cron/logs.png) Did this page help you? --- --- title: Webhook Relay Blog | WebhookRelay meta: "og: title": "Webhook Relay Blog" description: Guides, tutorials and comparisons for receiving, testing, securing and routing webhooks. url: https://webhookrelay.com/blog-all.md file: /blog-all.md --- # **The Webhook Relay Blog ** Guides, tutorials and comparisons for receiving, testing, securing, transforming and routing webhooks — from localhost to production. [**Guides**

**How to Send a Webhook from Google Forms (Apps Script)**

Google Forms has no built-in webhooks, but a few lines of Apps Script and an on-form-submit trigger will POST every response to your URL. Copy-paste code.Jul 21, 2026•3 min read](https://webhookrelay.com/blog-all/blog/google-forms-webhook/) [**Guides**

**How to Create a Microsoft Teams Webhook (Workflows, 2026)**

Microsoft retired Office 365 Connectors. Here's the current way to create a Teams incoming webhook with Workflows (Power Automate), step by step.Jul 21, 2026•3 min read](https://webhookrelay.com/blog-all/blog/microsoft-teams-webhook/) [**Guides**

**How to Get a Discord Webhook URL**

Create a Discord webhook URL step by step: Server Settings, Integrations, pick a channel, copy the URL. Includes the format, a curl example and embeds.Jul 21, 2026•2 min read](https://webhookrelay.com/blog-all/blog/how-to-get-discord-webhook-url/) [**Guides**

**How to Get a Slack Webhook URL (Incoming Webhooks)**

Get a Slack incoming webhook URL step by step: create a Slack app, enable Incoming Webhooks, pick a channel, and post with curl. Includes the format.Jul 21, 2026•3 min read](https://webhookrelay.com/blog-all/blog/how-to-get-slack-webhook-url/) [**Guides**

**Twilio Webhooks: Setup, Payloads, Signature Validation & Testing**

A complete guide to Twilio webhooks: configuring SMS and voice, the form-encoded payload, validating X-Twilio-Signature, retries, and local testing.Jul 19, 2026•4 min read](https://webhookrelay.com/blog-all/blog/twilio-webhooks-guide/) [**Guides**

**Webhook Architecture Diagram: How Webhooks Flow End to End**

A clear webhook architecture diagram — from the sender that emits an event to the endpoint that receives it, plus queues, retries and signature checks.Jul 19, 2026•3 min read](https://webhookrelay.com/blog-all/blog/webhook-architecture-diagram/) [**Guides**

**Webhook vs WebSocket: What's the Difference?**

Webhooks and WebSockets both deliver real-time data, but one is a short one-way callback and the other a persistent connection. When to use each.Jul 19, 2026•3 min read](https://webhookrelay.com/blog-all/blog/webhook-vs-websocket/) [**Guides**

**Receiving Webhooks in CI/CD: GitHub Actions, Jenkins & Harness (2026)**

CI/CD runners are ephemeral and private, so providers can't reach them. How to receive webhooks in GitHub Actions, Jenkins and Harness behind a firewall.Jul 18, 2026•4 min read](https://webhookrelay.com/blog-all/blog/webhooks-in-ci-cd/) [**Comparisons**

**An ngrok Alternative for Webhooks: Persistent URLs & Team Broadcast (2026)**

An ngrok alternative for webhooks: a persistent URL that never changes, shared across your team so everyone receives the same Stripe or GitHub events.Jul 18, 2026•6 min read](https://webhookrelay.com/blog-all/blog/ngrok-alternative/) [**Tutorials**

**Splunk Webhook Alerts: Payload, Limits and Fan-Out**

Wire Splunk alert actions to webhooks: the exact JSON payload, the action's limits (no custom headers), URL allowlisting, and fan-out to Slack.Jul 7, 2026•3 min read](https://webhookrelay.com/blog-all/blog/splunk-webhook-alerts/) [**Tutorials**

**Confluence Webhooks: Your Options on Cloud and Data Center**

How to get webhooks out of Confluence — Data Center's native webhooks, Cloud's Automation web requests and Forge apps — plus payloads and testing.Jul 7, 2026•3 min read](https://webhookrelay.com/blog-all/blog/confluence-webhooks/) [**Tutorials**

**Zendesk Webhooks: Setup, Signatures and Payload Examples**

Create Zendesk webhooks the right way: triggers vs event subscriptions, the payload format, verifying X-Zendesk-Webhook-Signature, and retries.Jul 7, 2026•3 min read](https://webhookrelay.com/blog-all/blog/zendesk-webhooks-guide/) [**Tutorials**

**DocuSign Connect Webhooks: Events, HMAC and Retry Behavior**

A practical guide to DocuSign Connect webhooks: envelope and recipient events, the JSON payload, HMAC verification, and Connect's retry behavior.Jul 7, 2026•4 min read](https://webhookrelay.com/blog-all/blog/docusign-connect-webhooks/) [**Tutorials**

**WhatsApp Cloud API Webhooks: Setup, Verify Token and Payloads**

Set up WhatsApp Cloud API webhooks: the callback URL, the hub.challenge verify token handshake, payload examples and X-Hub-Signature-256 verification.Jul 7, 2026•4 min read](https://webhookrelay.com/blog-all/blog/whatsapp-cloud-api-webhooks/) [**Tutorials**

**Jira Webhooks: Setup, Payload and Security**

How to set up Jira webhooks — admin UI vs REST API, JQL filters, the payload format, X-Hub-Signature verification and testing. A complete, practical guide.Jul 7, 2026•5 min read](https://webhookrelay.com/blog-all/blog/jira-webhooks-guide/) [**Tutorials**

**Trigger a Self-Hosted AI Agent with Webhooks (No Public IP)**

Run a local AI agent on a Raspberry Pi or Mac mini behind NAT and trigger it with GitHub or Stripe webhooks — no port forwarding and no public IP.Jul 2, 2026•4 min read](https://webhookrelay.com/blog-all/blog/self-hosted-ai-agent-webhooks/) [**Product**

**Receive Emails as Webhooks: Inbound Email Parsing & Forwarding**

Webhook Relay can receive email and turn it into a webhook — each message parsed to JSON (from, subject, text, HTML, attachments) and sent to your endpoint.Jun 28, 2026•2 min read](https://webhookrelay.com/blog-all/blog/receive-emails-as-webhooks/) [**Product**

**Durable webhook retries: a live demo of webhooks that never give up**

We launched durable retries — webhooks persisted and retried with backoff for up to 30 days. We built an endpoint that fails 80% of the time to prove it.Jun 16, 2026•6 min read](https://webhookrelay.com/blog-all/blog/durable-webhook-retries-demo/) [**Integrations**

**Send a Webhook to Trello**

Send any webhook to Trello. Transform an incoming webhook into the Trello API format in flight and forward it — no glue server, no code to maintain.Jun 11, 2026•2 min read](https://webhookrelay.com/blog-all/blog/webhook-to-trello/) [**Integrations**

**Send a Webhook to Jira**

Send any webhook to Jira. Transform an incoming webhook into the Jira API format in flight and forward it — no glue server, no code to maintain.Jun 11, 2026•2 min read](https://webhookrelay.com/blog-all/blog/webhook-to-jira/) [**Integrations**

**Send a Webhook to Opsgenie**

Send any webhook to Opsgenie. Transform an incoming webhook into the Opsgenie API format in flight and forward it — no glue server, no code to maintain.Jun 11, 2026•2 min read](https://webhookrelay.com/blog-all/blog/webhook-to-opsgenie/) [**Integrations**

**Send a Webhook to Salesforce**

Send any webhook to Salesforce. Transform an incoming webhook into the Salesforce API format in flight and forward it — no glue server, no code to maintain.Jun 11, 2026•2 min read](https://webhookrelay.com/blog-all/blog/webhook-to-salesforce/) [**Integrations**

**Send a Webhook to HubSpot**

Send any webhook to HubSpot. Transform an incoming webhook into the HubSpot API format in flight and forward it — no glue server, no code to maintain.Jun 11, 2026•2 min read](https://webhookrelay.com/blog-all/blog/webhook-to-hubspot/) [**Tutorials**

**Test Webflow Webhooks Locally**

Test Webflow webhooks locally and receive them on localhost without deploying. Inspect the real payload, forward to your handler, and verify the signature.Jun 11, 2026•5 min read](https://webhookrelay.com/blog-all/blog/receive-webflow-webhooks-locally/) [**Tutorials**

**Test Okta Webhooks Locally**

Test Okta webhooks locally and receive them on localhost without deploying. Inspect the real payload, forward to your handler, and verify the signature.Jun 11, 2026•5 min read](https://webhookrelay.com/blog-all/blog/receive-okta-webhooks-locally/) [**Tutorials**

**Test Dropbox Webhooks Locally**

Test Dropbox webhooks locally and receive them on localhost without deploying. Inspect the real payload, forward to your handler, and verify the signature.Jun 11, 2026•5 min read](https://webhookrelay.com/blog-all/blog/receive-dropbox-webhooks-locally/) [**Tutorials**

**Test Box Webhooks Locally**

Test Box webhooks locally and receive them on localhost without deploying. Inspect the real payload, forward to your handler, and verify the signature.Jun 11, 2026•5 min read](https://webhookrelay.com/blog-all/blog/receive-box-webhooks-locally/) [**Tutorials**

**Test Asana Webhooks Locally**

Test Asana webhooks locally and receive them on localhost without deploying. Inspect the real payload, forward to your handler, and verify the signature.Jun 11, 2026•5 min read](https://webhookrelay.com/blog-all/blog/receive-asana-webhooks-locally/) [**Tutorials**

**Test Zendesk Webhooks Locally**

Test Zendesk webhooks locally and receive them on localhost without deploying. Inspect the real payload, forward to your handler, and verify the signature.Jun 11, 2026•5 min read](https://webhookrelay.com/blog-all/blog/receive-zendesk-webhooks-locally/) [**Tutorials**

**Test Coinbase Commerce Webhooks Locally**

Test Coinbase Commerce webhooks locally without deploying. Inspect the real payload, forward it to your handler, and verify the X-CC-Webhook-Signature.Jun 11, 2026•5 min read](https://webhookrelay.com/blog-all/blog/receive-coinbase-commerce-webhooks-locally/) [**Tutorials**

**Test Adyen Webhooks Locally**

Test Adyen webhooks locally and receive them on localhost without deploying. Inspect the real payload, forward to your handler, and verify the signature.Jun 11, 2026•5 min read](https://webhookrelay.com/blog-all/blog/receive-adyen-webhooks-locally/) [**Tutorials**

**Datadog Webhook Tester — Test & Inspect Online**

Test and inspect Datadog webhooks online with a free webhook tester URL — capture real Datadog payloads, see what arrives, then forward locally.Jun 11, 2026•3 min read](https://webhookrelay.com/blog-all/blog/datadog-webhook-tester/) [**Tutorials**

**SendGrid Webhook Tester — Test & Inspect Online**

Test and inspect SendGrid webhooks online with a free webhook tester URL — capture real SendGrid payloads, see what arrives, then forward locally.Jun 11, 2026•3 min read](https://webhookrelay.com/blog-all/blog/sendgrid-webhook-tester/) [**Tutorials**

**Mailgun Webhook Tester — Test & Inspect Online**

Test and inspect Mailgun webhooks online with a free webhook tester URL — capture real Mailgun payloads, see what arrives, then forward locally.Jun 11, 2026•3 min read](https://webhookrelay.com/blog-all/blog/mailgun-webhook-tester/) [**Tutorials**

**Test Mailchimp Webhooks Locally**

Test Mailchimp webhooks locally and receive them on localhost without deploying. Inspect the real payload, forward to your handler, and verify the signature.Jun 11, 2026•5 min read](https://webhookrelay.com/blog-all/blog/receive-mailchimp-webhooks-locally/) [**Tutorials**

**Test Jira Webhooks Locally**

Test Jira webhooks locally and receive them on localhost without deploying. Inspect the real payload, forward to your handler, and verify the signature.Jun 11, 2026•5 min read](https://webhookrelay.com/blog-all/blog/receive-jira-webhooks-locally/) [**Tutorials**

**Test Zoom Webhooks Locally**

Test Zoom webhooks locally and receive them on localhost without deploying. Inspect the real payload, forward to your handler, and verify the signature.Jun 11, 2026•5 min read](https://webhookrelay.com/blog-all/blog/receive-zoom-webhooks-locally/) [**Tutorials**

**Test DocuSign Webhooks Locally**

Test DocuSign webhooks locally and receive them on localhost without deploying. Inspect the real payload, forward to your handler, and verify the signature.Jun 11, 2026•5 min read](https://webhookrelay.com/blog-all/blog/receive-docusign-webhooks-locally/) [**Tutorials**

**Test Plaid Webhooks Locally**

Test Plaid webhooks locally and receive them on localhost without deploying. Inspect the real payload, forward to your handler, and verify the signature.Jun 11, 2026•5 min read](https://webhookrelay.com/blog-all/blog/receive-plaid-webhooks-locally/) [**Tutorials**

**Test WorkOS Webhooks Locally**

Test WorkOS webhooks locally and receive them on localhost without deploying. Inspect the real payload, forward to your handler, and verify the signature.Jun 11, 2026•5 min read](https://webhookrelay.com/blog-all/blog/receive-workos-webhooks-locally/) [**Integrations**

**Send a Webhook to Mattermost**

Send any webhook to Mattermost. Forward an incoming webhook into a channel, transforming the raw payload into the Mattermost webhook format in flight.Jun 11, 2026•2 min read](https://webhookrelay.com/blog-all/blog/webhook-to-mattermost/) [**Integrations**

**Send a Webhook to Datadog: Post Events From Any Source**

Send any webhook to Datadog. Transform an incoming webhook into a Datadog Events API request in flight to post events and alerts — no glue server.Jun 11, 2026•2 min read](https://webhookrelay.com/blog-all/blog/webhook-to-datadog/) [**Integrations**

**Send a Webhook to PagerDuty: Trigger Incidents From Any Event**

Send any webhook to PagerDuty. Transform an incoming webhook into a PagerDuty Events API v2 alert in flight to trigger or resolve incidents.Jun 11, 2026•2 min read](https://webhookrelay.com/blog-all/blog/webhook-to-pagerduty/) [**Integrations**

**Send a Webhook to BigQuery: Stream Events Into a Table**

Stream incoming webhooks straight into Google BigQuery. Insert each event as a row for real-time analytics — no pipeline to build or maintain.Jun 11, 2026•2 min read](https://webhookrelay.com/blog-all/blog/webhook-to-bigquery/) [**Integrations**

**How to Archive Webhooks to Google Cloud Storage (GCS)**

Store every incoming webhook in Google Cloud Storage with Service Connections — no code. Keep an auditable payload archive for compliance and replay.Jun 11, 2026•2 min read](https://webhookrelay.com/blog-all/blog/webhook-to-gcs/) [**Integrations**

**Send a Webhook to Airtable**

Send any webhook to Airtable. Forward incoming webhooks into an Airtable base, transforming the payload into the Airtable API format in flight.Jun 11, 2026•3 min read](https://webhookrelay.com/blog-all/blog/webhook-to-airtable/) [**Integrations**

**Send a Webhook to Notion**

Send any webhook to Notion. Forward incoming webhooks into a Notion database, transforming the payload into the Notion API format in flight.Jun 11, 2026•3 min read](https://webhookrelay.com/blog-all/blog/webhook-to-notion/) [**Tutorials**

**Razorpay Webhook Tester — Test & Inspect Online**

Test and inspect Razorpay webhooks online with a free webhook tester URL — capture real Razorpay payloads, read the signature header, then forward locally.Jun 11, 2026•3 min read](https://webhookrelay.com/blog-all/blog/razorpay-webhook-tester/) [**Tutorials**

**Calendly Webhook Tester — Test & Inspect Online**

Test and inspect Calendly webhooks online with a free webhook tester URL — capture real Calendly payloads, read the signature header, then forward locally.Jun 11, 2026•3 min read](https://webhookrelay.com/blog-all/blog/calendly-webhook-tester/) [**Tutorials**

**Typeform Webhook Tester — Test & Inspect Online**

Test and inspect Typeform webhooks online with a free webhook tester URL — capture real Typeform payloads, read the signature header, then forward locally.Jun 11, 2026•3 min read](https://webhookrelay.com/blog-all/blog/typeform-webhook-tester/) [**Tutorials**

**Intercom Webhook Tester — Test & Inspect Online**

Test and inspect Intercom webhooks online with a free webhook tester URL — capture real Intercom payloads, read the signature header, then forward locally.Jun 11, 2026•3 min read](https://webhookrelay.com/blog-all/blog/intercom-webhook-tester/) [**Tutorials**

**Linear Webhook Tester — Test & Inspect Online**

Test and inspect Linear webhooks online with a free webhook tester URL — capture real Linear payloads, read the signature header, then forward locally.Jun 11, 2026•3 min read](https://webhookrelay.com/blog-all/blog/linear-webhook-tester/) [**Tutorials**

**HubSpot Webhook Tester — Test & Inspect Online**

Test and inspect HubSpot webhooks online with a free webhook tester URL — capture real HubSpot payloads, read the signature header, then forward locally.Jun 11, 2026•3 min read](https://webhookrelay.com/blog-all/blog/hubspot-webhook-tester/) [**Tutorials**

**Vercel Webhook Tester — Test & Inspect Online**

Test and inspect Vercel webhooks online with a free webhook tester URL — capture real Vercel payloads, read the signature header, then forward locally.Jun 11, 2026•3 min read](https://webhookrelay.com/blog-all/blog/vercel-webhook-tester/) [**Tutorials**

**Clerk Webhook Tester — Test & Inspect Online**

Test and inspect Clerk webhooks online with a free webhook tester URL — capture real Clerk payloads, read the signature header, then forward locally.Jun 11, 2026•3 min read](https://webhookrelay.com/blog-all/blog/clerk-webhook-tester/) [**Tutorials**

**Slack Webhook Tester — Test & Inspect Online**

Test and inspect Slack webhooks online with a free webhook tester URL — capture real Slack payloads, read the signature header, then forward locally.Jun 11, 2026•3 min read](https://webhookrelay.com/blog-all/blog/slack-webhook-tester/) [**Tutorials**

**PayPal Webhook Tester — Test & Inspect Online**

Test and inspect PayPal webhooks online with a free webhook tester URL — capture real PayPal payloads, read the signature header, then forward locally.Jun 11, 2026•3 min read](https://webhookrelay.com/blog-all/blog/paypal-webhook-tester/) [**Tutorials**

**Sentry Webhook Tester — Test & Inspect Online**

Test and inspect Sentry webhooks online with a free webhook tester URL — capture real Sentry payloads, read the signature header, then forward locally.Jun 11, 2026•3 min read](https://webhookrelay.com/blog-all/blog/sentry-webhook-tester/) [**Tutorials**

**Square Webhook Tester — Test & Inspect Online**

Test and inspect Square webhooks online with a free webhook tester URL — capture real Square payloads, read the signature header, then forward locally.Jun 11, 2026•3 min read](https://webhookrelay.com/blog-all/blog/square-webhook-tester/) [**Tutorials**

**GitLab Webhook Tester — Test & Inspect Online**

Test and inspect GitLab webhooks online with a free webhook tester URL — capture real GitLab payloads, read the signature header, then forward locally.Jun 11, 2026•3 min read](https://webhookrelay.com/blog-all/blog/gitlab-webhook-tester/) [**Tutorials**

**Twilio Webhook Tester — Test & Inspect Online**

Test and inspect Twilio webhooks online with a free webhook tester URL — capture real Twilio payloads, read the signature header, then forward locally.Jun 11, 2026•3 min read](https://webhookrelay.com/blog-all/blog/twilio-webhook-tester/) [**Tutorials**

**Shopify Webhook Tester — Test & Inspect Online**

Test and inspect Shopify webhooks online with a free webhook tester URL — capture real Shopify payloads, read the signature header, then forward locally.Jun 11, 2026•3 min read](https://webhookrelay.com/blog-all/blog/shopify-webhook-tester/) [**Tutorials**

**GitHub Webhook Tester — Test & Inspect Online**

Test and inspect GitHub webhooks online with a free webhook tester URL — capture real GitHub payloads, read the signature header, then forward locally.Jun 11, 2026•3 min read](https://webhookrelay.com/blog-all/blog/github-webhook-tester/) [**Tutorials**

**Stripe Webhook Tester — Test & Inspect Online**

Test and inspect Stripe webhooks online with a free webhook tester URL — capture real Stripe payloads, read the signature header, then forward locally.Jun 11, 2026•3 min read](https://webhookrelay.com/blog-all/blog/stripe-webhook-tester/) [**Tutorials**

**Test Intercom Webhooks Locally**

Test Intercom webhooks locally on localhost without deploying. Inspect the real topic payload, forward to your handler, and verify the X-Hub-Signature.Jun 11, 2026•6 min read](https://webhookrelay.com/blog-all/blog/receive-intercom-webhooks-locally/) [**Tutorials**

**Test Stripe Connect Webhooks Locally**

Test Stripe Connect webhooks locally on localhost without deploying. Capture connected-account events, forward to your handler, and verify the Stripe-Signature.Jun 11, 2026•7 min read](https://webhookrelay.com/blog-all/blog/receive-stripe-connect-webhooks-locally/) [**Tutorials**

**Test HubSpot Webhooks Locally**

Test HubSpot webhooks on localhost without deploying. Inspect the batched payload and verify the X-HubSpot-Signature-v3 header on every request.Jun 11, 2026•6 min read](https://webhookrelay.com/blog-all/blog/receive-hubspot-webhooks-locally/) [**Comparisons**

**Stripe CLI Alternative for Testing Webhooks Across Every Provider**

A Stripe CLI alternative for teams: one stable URL that receives Stripe and every other provider's webhooks, shareable across the whole team.Jun 11, 2026•6 min read](https://webhookrelay.com/blog-all/blog/stripe-cli-alternative/) [**Integrations**

**Webhook to Google Sheets: Transform & Forward Any Payload to a Row**

Send a webhook to Google Sheets. Forward an incoming webhook into a spreadsheet with an Apps Script Web App, transforming the payload in flight.Jun 11, 2026•6 min read](https://webhookrelay.com/blog-all/blog/webhook-to-google-sheets/) [**Integrations**

**Webhook to Telegram: Transform & Forward Any Payload to a Chat**

Send a webhook to Telegram. Forward an incoming webhook into a chat, transforming the raw payload into the Bot API's sendMessage format in flight.Jun 11, 2026•5 min read](https://webhookrelay.com/blog-all/blog/webhook-to-telegram/) [**Tutorials**

**Test Linear Webhooks Locally**

Test Linear webhooks on localhost without deploying. Inspect the real payload, forward to your handler, and verify the Linear-Signature header.Jun 11, 2026•6 min read](https://webhookrelay.com/blog-all/blog/receive-linear-webhooks-locally/) [**Tutorials**

**Test WhatsApp Webhooks Locally**

Test WhatsApp Cloud API webhooks locally on localhost. Pass Meta's verify-token GET handshake, inspect message payloads, and verify the X-Hub-Signature-256.Jun 11, 2026•6 min read](https://webhookrelay.com/blog-all/blog/receive-whatsapp-webhooks-locally/) [**Tutorials**

**Receive Mollie Webhooks Locally**

Test Mollie webhooks on localhost without deploying. Mollie posts only the payment id, so inspect the request and fetch status from the API.Jun 11, 2026•6 min read](https://webhookrelay.com/blog-all/blog/receive-mollie-webhooks-locally/) [**Tutorials**

**Receive Razorpay Webhooks Locally**

Test Razorpay webhooks on localhost without deploying. Inspect the real payment.captured payload and verify the X-Razorpay-Signature header.Jun 11, 2026•6 min read](https://webhookrelay.com/blog-all/blog/receive-razorpay-webhooks-locally/) [**Tutorials**

**Receive Auth0 Webhooks Locally**

Test Auth0 webhooks locally by streaming Auth0 Log Streams to localhost. Inspect the real batch of log events, forward to your handler, and verify the token.Jun 11, 2026•6 min read](https://webhookrelay.com/blog-all/blog/receive-auth0-webhooks-locally/) [**Tutorials**

**Receive Vercel Webhooks Locally**

Test Vercel webhooks and run your handler on localhost. Inspect the real deployment payload and verify the x-vercel-signature header.Jun 11, 2026•6 min read](https://webhookrelay.com/blog-all/blog/receive-vercel-webhooks-locally/) [**Comparisons**

**An expose.dev Alternative for Webhooks and Tunnels (2026)**

Compare Expose (expose.dev) and Webhook Relay for tunneling localhost and forwarding webhooks — stable URLs, transforms, fan-out, retries, free plan.Jun 10, 2026•4 min read](https://webhookrelay.com/blog-all/blog/expose-dev-alternative/) [**Comparisons**

**A Reliable localtunnel Alternative for Webhooks and Tunnels (2026)**

Compare localtunnel and Webhook Relay for exposing localhost and forwarding webhooks — stable URLs, retries, request inspection, and a free plan.Jun 10, 2026•4 min read](https://webhookrelay.com/blog-all/blog/localtunnel-alternative/) [**Tutorials**

**Receive Datadog Webhooks Locally**

Test Datadog webhooks on localhost without deploying. Inspect the real alert payload and forward monitor notifications straight to your handler.Jun 10, 2026•6 min read](https://webhookrelay.com/blog-all/blog/receive-datadog-webhooks-locally/) [**Tutorials**

**Receive Mailgun Webhooks Locally**

Test Mailgun webhooks on localhost without deploying. Inspect the real event payload, forward delivered and bounced events, and verify the signature.Jun 10, 2026•6 min read](https://webhookrelay.com/blog-all/blog/receive-mailgun-webhooks-locally/) [**Tutorials**

**Test Calendly Webhooks Locally**

Test Calendly webhooks on localhost without deploying. Create the subscription, inspect the real invitee.created payload, and verify the signature.Jun 10, 2026•6 min read](https://webhookrelay.com/blog-all/blog/receive-calendly-webhooks-locally/) [**Tutorials**

**Test Typeform Webhooks Locally**

Test Typeform webhooks on localhost without deploying. Add the webhook in Typeform, inspect the real form_response payload, and verify the signature.Jun 10, 2026•6 min read](https://webhookrelay.com/blog-all/blog/receive-typeform-webhooks-locally/) [**Comparisons**

**A Convoy Alternative for Delivering Webhooks Into Private Infrastructure**

Compare Convoy and Webhook Relay. Convoy is a self-hostable webhook gateway; Webhook Relay is hosted and also delivers to localhost and private networks.Jun 10, 2026•4 min read](https://webhookrelay.com/blog-all/blog/convoy-alternative/) [**Comparisons**

**A Zapier Webhooks Alternative for Developer-Grade Webhook Delivery**

Compare Webhooks by Zapier and Webhook Relay. Zapier is no-code automation; Webhook Relay is developer webhook infrastructure with private delivery.Jun 10, 2026•5 min read](https://webhookrelay.com/blog-all/blog/zapier-webhooks-alternative/) [**Tutorials**

**Test Clerk Webhooks Locally**

Test Clerk webhooks locally and receive them on localhost without deploying. Inspect the real payload, forward to your handler, and verify the Svix signature.Jun 10, 2026•6 min read](https://webhookrelay.com/blog-all/blog/receive-clerk-webhooks-locally/) [**Tutorials**

**Test Sentry Webhooks Locally**

Test Sentry webhooks on localhost without deploying. Inspect the real payload, forward to your handler, and verify the Sentry-Hook-Signature.Jun 10, 2026•6 min read](https://webhookrelay.com/blog-all/blog/receive-sentry-webhooks-locally/) [**Tutorials**

**Test SendGrid Webhooks Locally**

Test the SendGrid Event Webhook on localhost without deploying. Inspect the real JSON event array and verify the signed webhook signature.Jun 10, 2026•6 min read](https://webhookrelay.com/blog-all/blog/receive-sendgrid-webhooks-locally/) [**Tutorials**

**Receive Square Webhooks Locally**

Receive Square webhooks on localhost without deploying. Inspect the real payment.created payload and verify the HMAC signature on every request.Jun 10, 2026•6 min read](https://webhookrelay.com/blog-all/blog/receive-square-webhooks-locally/) [**Integrations**

**How to Archive Webhooks to Amazon S3 (Store Every Payload)**

Store every incoming webhook in Amazon S3 with Service Connections — no code. Keep an auditable archive of payloads for compliance and debugging.Jun 10, 2026•2 min read](https://webhookrelay.com/blog-all/blog/webhook-to-s3/) [**Integrations**

**Send a Webhook as Email: Forward Any Event to Your Inbox**

Turn incoming webhooks into email notifications. Forward an event, transform it in flight, and send a formatted email via SendGrid, Mailgun or Postmark.Jun 10, 2026•5 min read](https://webhookrelay.com/blog-all/blog/webhook-to-email/) [**Integrations**

**Send a Webhook to Microsoft Teams**

Send any incoming webhook to Microsoft Teams. Transform the raw payload into Teams message JSON in flight and post it to an Incoming Webhook URL.Jun 10, 2026•6 min read](https://webhookrelay.com/blog-all/blog/webhook-to-microsoft-teams/) [**Guides**

**Debug Webhooks: A Practical Troubleshooting Guide (2026)**

Troubleshoot webhooks step by step: events that never arrive, signature mismatches, content-type and body parsing, duplicates, timeouts and retries.Jun 10, 2026•5 min read](https://webhookrelay.com/blog-all/blog/how-to-debug-webhooks/) [**Guides**

**Webhook Authentication: HMAC, Tokens, mTLS and IP Allow-listing Compared**

A practical guide to webhook authentication: HMAC signatures, shared-secret tokens, basic auth, mTLS, IP allow-listing and JWT, and when to use each.Jun 10, 2026•5 min read](https://webhookrelay.com/blog-all/blog/webhook-authentication/) [**Guides**

**Webhook Retries and Idempotency: A Practical Guide**

Why webhook deliveries fail, how providers retry with exponential backoff, and how to build idempotent handlers that dedupe on event ID.Jun 10, 2026•6 min read](https://webhookrelay.com/blog-all/blog/webhook-retries-and-idempotency/) [**Comparisons**

**A Beeceptor Alternative for Inspecting and Forwarding Webhooks (2026)**

Compare Beeceptor and Webhook Relay: a free webhook bin with custom responses, plus forwarding to localhost and private servers, retries and fan-out.Jun 10, 2026•4 min read](https://webhookrelay.com/blog-all/blog/beeceptor-alternative/) [**Comparisons**

**A Cloudflare Tunnel Alternative for Webhooks and Localhost Tunnels (2026)**

Compare Cloudflare Tunnel and Webhook Relay for exposing localhost and forwarding webhooks — a stable URL with no domain to manage, plus retries.Jun 10, 2026•5 min read](https://webhookrelay.com/blog-all/blog/cloudflare-tunnel-alternative/) [**Comparisons**

**A Pipedream Alternative for Webhook Routing and Private Delivery**

Compare Pipedream and Webhook Relay. Pipedream is no-code automation; Webhook Relay is webhook infrastructure that reaches localhost and private networks.Jun 10, 2026•4 min read](https://webhookrelay.com/blog-all/blog/pipedream-alternative/) [**Comparisons**

**A Svix Alternative for Receiving and Forwarding Webhooks Into Private Infrastructure**

Compare Svix and Webhook Relay. Svix sends webhooks to your customers; Webhook Relay receives third-party webhooks and forwards them to private networks.Jun 10, 2026•4 min read](https://webhookrelay.com/blog-all/blog/svix-alternative/) [**Integrations**

**How to Send Webhooks to Google Cloud Pub/Sub**

Publish incoming webhooks straight to a Google Cloud Pub/Sub topic with Service Connections — no Cloud Function to write. Build event-driven pipelines.Jun 10, 2026•2 min read](https://webhookrelay.com/blog-all/blog/webhook-to-pubsub/) [**Integrations**

**How to Send Webhooks to AWS SQS (No Consumer Code)**

Forward incoming webhooks straight into an Amazon SQS queue with Service Connections — no Lambda and no consumer to write. Decouple and buffer safely.Jun 10, 2026•2 min read](https://webhookrelay.com/blog-all/blog/webhook-to-sqs/) [**Integrations**

**Send a Webhook to Discord**

Send any webhook to Discord. Forward an incoming webhook to a Discord channel, transforming the raw payload into Discord's content format in flight.Jun 10, 2026•5 min read](https://webhookrelay.com/blog-all/blog/webhook-to-discord/) [**Integrations**

**Send a Webhook to Slack**

Send any webhook to Slack. Forward an incoming webhook into a Slack channel, transforming the raw payload into Slack's text format in flight.Jun 10, 2026•5 min read](https://webhookrelay.com/blog-all/blog/webhook-to-slack/) [**Tutorials**

**Receive Slack Events Locally: Test Slack Webhooks on localhost**

Test Slack webhooks without deploying. Forward Events API requests to localhost, pass the url_verification challenge, and verify X-Slack-Signature.Jun 10, 2026•5 min read](https://webhookrelay.com/blog-all/blog/receive-slack-events-locally/) [**Tutorials**

**Receive Twilio Webhooks Locally**

Test Twilio webhooks without deploying. Forward SMS and voice callbacks to localhost, handle the form-encoded body, and verify X-Twilio-Signature.Jun 10, 2026•5 min read](https://webhookrelay.com/blog-all/blog/receive-twilio-webhooks-locally/) [**Tutorials**

**Receive GitHub Webhooks Locally**

Receive GitHub webhooks locally and test them on localhost without deploying. Inspect the real payload, forward to your handler, and verify the signature.Jun 10, 2026•5 min read](https://webhookrelay.com/blog-all/blog/receive-github-webhooks-locally/) [**Tutorials**

**Receive GitLab Webhooks Locally**

Receive GitLab webhooks locally with no public IP. Inspect the real payload, forward push and merge request events to localhost, and verify the token.Jun 10, 2026•5 min read](https://webhookrelay.com/blog-all/blog/receive-gitlab-webhooks-locally/) [**Integrations**

**TradingView Webhooks: Automate Alerts to Discord, Slack & More**

Send TradingView alerts anywhere with webhooks. Step-by-step setup to forward alerts to Discord, Slack, email or a trading API, with payload formatting.Jun 10, 2026•3 min read](https://webhookrelay.com/blog-all/blog/trading-view/) [**Guides**

**How to Verify a Webhook Signature (HMAC SHA256)**

Verify webhook signatures so you only trust authentic requests. How HMAC SHA256 works, how GitHub and Stripe do it, plus a Node.js example.Jun 10, 2026•4 min read](https://webhookrelay.com/blog-all/blog/verify-webhook-signature/) [**Guides**

**Webhooks vs API: What's the Difference?**

A webhook pushes, an API pulls. The real differences, when to use each, code examples for both, and the reliability trade-offs nobody warns you about.Jun 10, 2026•10 min read](https://webhookrelay.com/blog-all/blog/webhooks-vs-api/) [**Guides**

**How to Test Webhooks: The Complete Guide (2026)**

A practical guide to testing webhooks: get an instant URL, inspect payloads, receive webhooks on localhost, replay requests and verify signatures.Jun 10, 2026•4 min read](https://webhookrelay.com/blog-all/blog/how-to-test-webhooks/) [**Comparisons**

**A smee.io Alternative for Reliable Webhook Forwarding**

smee.io is great for quick GitHub tests but is dev-only. Webhook Relay is a production-ready alternative: stable URLs, retries, and private delivery.Jun 10, 2026•2 min read](https://webhookrelay.com/blog-all/blog/smee-io-alternative/) [**Comparisons**

**A Hookdeck Alternative for Delivering Webhooks to Private Infrastructure**

Compare Hookdeck and Webhook Relay. Both queue, retry and transform — but Webhook Relay also delivers to localhost and private networks, and tunnels.Jun 10, 2026•2 min read](https://webhookrelay.com/blog-all/blog/hookdeck-alternative/) [**Comparisons**

**A RequestBin Alternative: Free Webhook Inspector, No Signup**

The original free RequestBin is gone. Webhook Bin is a free alternative — an instant URL, real-time request inspection, then forwarding to localhost.Jun 10, 2026•2 min read](https://webhookrelay.com/blog-all/blog/requestbin-alternative/) [**Comparisons**

**A webhook.site Alternative for Testing and Forwarding Webhooks**

A webhook.site alternative that does more than inspect: an instant webhook URL, real-time requests, then forwarding to localhost or a private server.Jun 10, 2026•2 min read](https://webhookrelay.com/blog-all/blog/webhook-site-alternative/) [**Tutorials**

**Archive AWS GuardDuty Findings to GCP Cloud Storage**

Route AWS GuardDuty security findings to Google Cloud Storage using Webhook Relay Service Connections for long-term retention and cross-cloud analysisMar 7, 2026•5 min read](https://webhookrelay.com/blog-all/blog/guardduty-to-gcs-archival/) [**Product**

**Introducing Service Connections**

Connect AWS, GCP, and Azure services through Webhook Relay buckets with fast, flexible cloud-to-cloud routing.Feb 25, 2026•1 min read](https://webhookrelay.com/blog-all/blog/introducing_service_connections/) [**Pricing**

**Introducing extra webhook packages**

Purchase additional webhook capacity directly from your current plan tierFeb 1, 2026•2 min read](https://webhookrelay.com/blog-all/blog/extra-webhook-packages/) [**Comparisons**

**Azure Functions vs Webhook Relay: Why I stopped overengineering webhooks**

A practical comparison of Azure Functions and Webhook Relay for webhook processing, with code examples showing the difference in setup complexity.Feb 1, 2026•6 min read](https://webhookrelay.com/blog-all/blog/azure-functions-vs-webhook-relay/) [**Automation**

**How Lightning AI uses WHR to ship webhooks to the entire team**

How Lightning AI broadcasts Stripe webhooks to every developer's laptop and staging environment simultaneously — with zero setup per developer.Jan 9, 2026•6 min read](https://webhookrelay.com/blog-all/blog/lightning-ai-company-webhooks-setup/) [**Python**

**Receive Shopify webhooks on Flask API**

How to get Shopify webhooks working on your local Flask app, with proper signature verificationFeb 15, 2025•3 min read](https://webhookrelay.com/blog-all/blog/receiving-shopify-webhooks-flask-api/) [**Forwarding**

**What is a Webhook? Definition, Examples & How to Set One Up**

A webhook is an automated HTTP request a service sends when an event happens. How webhooks work, examples, and how to set one up in Node.js.Dec 12, 2024•9 min read](https://webhookrelay.com/blog-all/blog/what-is-webhook/) [**Automation**

**Automatically transform webhook payloads**

Automatically transform webhook payloads using AI in Webhook Relay. Step-by-step guide to convert webhook data between different formats without coding.Nov 13, 2024•2 min read](https://webhookrelay.com/blog-all/blog/auto-transform-webhook/) [**Automation**

**Static IPs for outgoing webhooks**

How to setup static IPs for webhook calls to enable whitelistingOct 8, 2024•1 min read](https://webhookrelay.com/blog-all/blog/static-ip/) [**Security**

**Webhook Security: Best Practices to Secure Your Webhooks**

Essential measures to fortify your webhooks — security best practices for reliable, safe data transfer that protect your applications from attack.Oct 22, 2023•5 min read](https://webhookrelay.com/blog-all/blog/webhook-security/) [**Webdev**

**Airtable integrations: inserting rows**

How to setup Airtable on setting up HTML contact form with Airtable code webhook integrationAug 7, 2023•4 min read](https://webhookrelay.com/blog-all/blog/airtable-integrations/) [**Webhooks**

**Receive emails on new Stripe subscribers**

It's nice to get Stripe notifications on new payments however we can turn any Stripe into an emailJun 25, 2022•3 min read](https://webhookrelay.com/blog-all/blog/stripe-webhook-to-email/) [**Kubernetes**

**Managed GKE control-plane failure resulting in platform outage on 10th May, 2022**

An RCA on how the managed GKE control-plane failure brought down the platformMay 13, 2022•5 min read](https://webhookrelay.com/blog-all/blog/may-10th-outage-gke-controlplane/) [**Automation**

**How to install and run a dockerized Jenkins CI with webhook support**

A quick tutorial on how to setup a Jenkins CI server using Docker, Synpse and Webhook Relay to have remote SSH access and secure webhooks•5 min read](https://webhookrelay.com/blog-all/blog/install-jenkins-ci-docker/) [**Ops**

**Self-hosted business intelligence with Metabase**

Setting up self-hosted Metabase on-premNov 11, 2021•3 min read](https://webhookrelay.com/blog-all/blog/setting-up-selfhosted-metabase/) [**Pricing**

**Changes to our prices for new customers**

As of 1st of May 2021, new WHR subscribers will be subject to new prices.Apr 12, 2021•1 min read](https://webhookrelay.com/blog-all/blog/pricing-changes/) [**Automation**

**Providing access to Kubernetes through tunnels in one of the largest cities in Lithuania**

How Lithuania's transport operator uses Webhook Relay tunnels to reach private Kubernetes clusters with TLS pass-through and an ingress.Nov 10, 2020•3 min read](https://webhookrelay.com/blog-all/blog/tunnels-to-kubernetes/) [**Automation**

**Ingesting Facebook webhooks (challenge & verification)**

How to receive Facebook webhooks and do verification for challenge and tokenSep 22, 2020•2 min read](https://webhookrelay.com/blog-all/blog/ingesting-facebook-webhooks/) [**Webdev**

**CDN types and setting them up (Vue, React)**

CDN (content delivery network) types and how to set one up (Vue, React)Aug 27, 2020•5 min read](https://webhookrelay.com/blog-all/blog/cdn-types-and-setup/) [**Webhooks**

**Running Webhook Relay agent with Podman**

A short guide how to run Webhook Relay agent with PodmanJun 22, 2020•2 min read](https://webhookrelay.com/blog-all/blog/webhookrelayd-with-podman/) [**Automation**

**New feature announcement: domain-based endpoints**

Introducing new feature: domain based webhook endpointsMay 31, 2020•3 min read](https://webhookrelay.com/blog-all/blog/domain-based-webhook-endpoints/) [**Automation**

**Static IPs for webhook calls to enable whitelisting**

How to setup static IPs for webhook calls to enable whitelistingApr 2, 2020•4 min read](https://webhookrelay.com/blog-all/blog/static-ips-for-webhook-whitelisting/) [**Automation**

**Responding to API calls using Node-RED Webhook Relay node**

How to respond to API calls using Node-RED Webhook Relay nodeMar 2, 2020•8 min read](https://webhookrelay.com/blog-all/blog/responding-to-api-calls-on-nodered/) [**Automation**

**How Dotscience manages thousands of tunnels to create a better Data Science environment**

A case study on how Dotscience utilizes Webhook Relay tunnelsJan 20, 2020•3 min read](https://webhookrelay.com/blog-all/blog/dotscience-tunnels-jupyter/) [**Automation**

**Docker Compose update on Github webhook**

Learn how to update Docker Compose on push to Github using webhooksSep 2, 2019•5 min read](https://webhookrelay.com/blog-all/blog/docker-compose-update-on-github-webhooks/) [**Automation**

**Using Google Firestore for a Golang backend application**

Switching from internal KV store to a Google Firestore can be quick and easyAug 26, 2019•5 min read](https://webhookrelay.com/blog-all/blog/using-google-firestore-for-go-backend/) [**Automation**

**Automated Jenkins builds on GitHub pull request**

Configuration example on how to automatically start builds on GitHub pull requestApr 17, 2019•5 min read](https://webhookrelay.com/blog-all/blog/automated-github-pull-request-builds-on-jenkins/) [**Automation**

**Rules-based webhook filtering & routing**

Example use-case of rules-based routing and filtering for GitHub webhooksApr 2, 2019•4 min read](https://webhookrelay.com/blog-all/blog/webhook-rule-based-filters/) [**Automation**

**Introducing Cloudflare support for Home Assistant remote access**

Webhook Relay Home Assistant remote access add-on now support Cloudflare DomainsFeb 15, 2019•3 min read](https://webhookrelay.com/blog-all/blog/cloudflare-support-for-home-assistant/) [**Automation**

**Setting up simple, self-hosted & fast CI/CD solution with Drone.io**

A guide/tutorial on how to set up Drone as a self-hosted CI/CD solution for private projectsFeb 11, 2019•8 min read](https://webhookrelay.com/blog-all/blog/using-drone-for-simple-selfhosted-ci-cd/) [**Automation**

**Controlling TV with Google Home, IFTTT and Node-RED**

Easiest way to start controlling your TV with Google Home, IFTTT and Node-REDJan 29, 2019•4 min read](https://webhookrelay.com/blog-all/blog/google-home-ifttt-node-red/) [**Automation**

**Node-RED OwnTracks location tracking without public IP/MQTT**

How to get webhooks from OwnTracks to Node-RED without public IP or configuring NATJan 9, 2019•6 min read](https://webhookrelay.com/blog-all/blog/nodered-owntracks-direct/) [**Kubernetes**

**Secure webhooks to Jenkins on Kubernetes**

A tutorial on how to securely receive GitHub webhooks on your Jenkins inside a Kubernetes clusterDec 18, 2018•6 min read](https://webhookrelay.com/blog-all/blog/webhooks-to-jenkins-on-kubernetes/) [**Automation**

**Remote YouTube downloader Slack bot**

A short tutorial to help you build a remote YouTube video downloader using WebSockets and SlackDec 12, 2018•7 min read](https://webhookrelay.com/blog-all/blog/remote-tube-downloader/) [**Automation**

**Introducing WebSocket Server**

Listen for new webhooks directly from your application using websocketsDec 5, 2018•5 min read](https://webhookrelay.com/blog-all/blog/introducing-websocket-server/) [**Kubernetes**

**Rancher - push to deploy workflow with Keel**

Configuring push to deploy workflow with Rancher and KeelNov 12, 2018•5 min read](https://webhookrelay.com/blog-all/blog/rancher-push-to-deploy-workflow/) [**Documentation**

**Documenting your API with OpenAPI (Swagger) and Redoc**

API tooling review and a guide on how to document your API with Swagger's OpenAPI and RedocNov 5, 2018•5 min read](https://webhookrelay.com/blog-all/blog/openapi-redoc-tutorial/) [**Automation**

**Home Assistant remote access add-on**

Reverse tunnels for testing and development environmentsOct 12, 2018•3 min read](https://webhookrelay.com/blog-all/blog/hassio-tls-tunnels-duckdns/) [**Automation**

**Hassle-free remote access to Home Assistant on a Raspberry Pi**

How to connect to your Home Assistant without public IP or NAT configurationSep 3, 2018•3 min read](https://webhookrelay.com/blog-all/blog/home-assistant-remote-access/) [**Webhooks**

**How to receive Paypal webhooks on localhost**

Often when building an application that integrates with 3rd party services we need a way to receive webhooksAug 21, 2018•5 min read](https://webhookrelay.com/blog-all/blog/receiving-paypal-webhooks-localhost/) [**Redis**

**DevOps Use Case: Performing Redis maintenance in Kubernetes**

Use Redis-Commander with Webhook Relay ingress controller to access Redis in a Kubernetes clusterJul 23, 2018•3 min read](https://webhookrelay.com/blog-all/blog/kubernetes-redis-commander/) [**Automation**

**Auto deploy your Node.js app on push to GitHub**

Learn how to update your Node.js app on push to GitHub using webhooks on any virtual machine or your local computerJul 17, 2018•4 min read](https://webhookrelay.com/blog-all/blog/auto-deploy-on-git-push/) [**Webhooks**

**Mailgun webhook fan-out**

How to send mailgun webhooks to multiple destinationsMay 21, 2018•3 min read](https://webhookrelay.com/blog-all/blog/mailgun-webhook-fanout/) [**Kubernetes**

**Web Relay Ingress with Docker for Mac**

Web Relay ingress for Mac lets users expose their local services to the internet for testing and demoingJan 8, 2018•6 min read](https://webhookrelay.com/blog-all/blog/ingress-with-docker-for-mac/) [**Webhooks**

**How to receive Stripe webhooks on localhost**

Often when building an application that integrates with 3rd party services we need a way to receive webhooksDec 26, 2017•4 min read](https://webhookrelay.com/blog-all/blog/receiving-stripe-webhooks-localhost/) [**Automation**

**Receive Github webhooks on Jenkins without public IP**

A short tutorial on how to configure and receive Github webhooks on your jenkins instance even without a public IPNov 23, 2017•6 min read](https://webhookrelay.com/blog-all/blog/github-jenkins-guide/) [**Kubernetes**

**Keel - automated Kubernetes updates**

Automatically update kubernetes deployments on image pushJul 17, 2017•2 min read](https://webhookrelay.com/blog-all/blog/introducing-keel/) [**Automation**

**Introduction to Webhook Relay**

Reverse tunnels for testing and development environmentsMay 15, 2017•2 min read](https://webhookrelay.com/blog-all/blog/introduction/) --- --- title: Custom webhook domains | WebhookRelay meta: "og: title": "Custom webhook domains" description: Receive, process and forward webhooks using your own domain name. url: https://webhookrelay.com/docs/webhooks/custom-domains.md file: /docs/webhooks/custom-domains.md --- ![Stripes](https://webhookrelay.com/docs/webhooks/custom-domains/images/stripes.svg) Documentation **Fundamentals** # **Custom webhook domains** Receive, process and forward webhooks using your own domain name. **Don't have a domain? Check out our **[Custom Subdomains](https://webhookrelay.com/docs/webhooks/custom-domains/docs/webhooks/custom-subdomains/)** guide.** ## [Setting up custom domain](#setting-up-custom-domain) In many cases you would prefer to use your own domain when receiving the webhooks. It helps with branding and makes it easier to manage your webhooks. You can delete, transfer between buckets or update your domain at any time. ## [Step 1: Add your domain in Webhook Relay](#step-1-add-your-domain-in-webhook-relay) First, navigate to [Webhook Relay domains page](https://my.webhookrelay.com/domains/) and click on "Reserve domain" button. ![Custom Domains](https://webhookrelay.com/docs/webhooks/custom-domains/images/features/custom-domains/cover.png) In the form add your domain name and click on "Reserve" button. ## [Step 2: Configure your DNS](#step-2-configure-your-dns) Once the domain is added, you should see "Pending Action" badge next to your reserved domain: ![Pending Action](https://webhookrelay.com/docs/webhooks/custom-domains/images/features/custom-domains/pending-action.png) You will need to access your DNS provider. Then, create a CNAME record in your domain pointing at `hooks.webhookrelay.com`. Instructions on how to do this can be found in your DNS provider's documentation. How to do it in: - Cloudflare: [https://developers.cloudflare.com/dns/zone-setups/partial-setup/setup/](https://developers.cloudflare.com/dns/zone-setups/partial-setup/setup/) - Namecheap: [https://www.namecheap.com/support/knowledgebase/article.aspx/9646/2237/how-to-create-a-cname-record-for-your-domain/](https://www.namecheap.com/support/knowledgebase/article.aspx/9646/2237/how-to-create-a-cname-record-for-your-domain/) - Gandi: [https://docs.gandi.net/en/domain_names/common_operations/link_domain_to_website.html](https://docs.gandi.net/en/domain_names/common_operations/link_domain_to_website.html) ## [Step 3: Select your domain in the Webhook Relay bucket](#step-3-select-your-domain-in-the-webhook-relay-bucket) Last step is to select your domain in the Webhook Relay bucket's input settings: ![Select domain](https://webhookrelay.com/docs/webhooks/custom-domains/images/features/custom-domains/select-domain.png) Path is optional but it allows you to reuse same domain for multiple buckets and inputs by using different paths. Did this page help you? --- --- title: Connecting to websocket server | WebhookRelay meta: "og: title": "Connecting to websocket server" description: Webhook Relay websocket server allows your applications to directly process webhooks without having a public IP. url: https://webhookrelay.com/docs/webhooks/websocket-server.md file: /docs/webhooks/websocket-server.md --- ![Stripes](https://webhookrelay.com/docs/webhooks/websocket-server/images/stripes.svg) Documentation **Fundamentals** # **Connecting to websocket server** Webhook Relay websocket server allows your applications to directly process webhooks without having a public IP. ## [Protocol and Configuration](#protocol-and-configuration) Socket server allows connecting to Webhook Relay service directly from your application using WebSockets. Communication is done using JSON encoded messages. In order to start streaming, [create a new bucket](https://my.webhookrelay.com/buckets) with your desired name. Once you have a bucket, you can start receiving webhooks to your public endpoint (one public endpoint will be created by default upon bucket creation). > WebSockets will receive events with webhook data when both no outputs are defined **or** for every **internal** output specified. If you only have public outputs, neither WebSockets nor relay agent will receive them. ### [Step 1: Connect](#step-1-connect) Your API keys allow multiple simultaneous connections. Connect to: ``` wss://my.webhookrelay.com/v1/socket ``` ### [Step 2: Authenticate](#step-2-authenticate) You must authenticate before you can make any other requests. Generate a new key & secret pair in your tokens page ([https://my.webhookrelay.com/tokens](https://my.webhookrelay.com/tokens)). To authenticate, send: ``` { "action":"auth", "key":"YOUR_KEY", "secret":"YOUR_SECRET" } ``` Once authenticated, you will receive the following message: ``` { "type": "status", "status": "authenticated", "message": "connected successfully, subscribe to buckets" } ``` ### [Step 3: Subscribe to webhooks stream](#step-3-subscribe-to-webhooks-stream) Once authenticated, you can request a stream. Buckets ([https://my.webhookrelay.com/buckets](https://my.webhookrelay.com/buckets)) are used for grouping and routing. You can request multiple bucket streams. To subscribe, send: ``` { "action":"subscribe", "buckets": [ "my-1-bucket-name", "my-2-bucket-id" ] } ``` > Field `buckets` works as a filter, checking for bucket ID or bucket name. To subscribe to all buckets in your account, send only `{"action":"subscribe"}` message. Once subscribed, you will receive the following message, confirming your stream: ``` { "type": "status", "status": "subscribed", "message": "subscribed to buckets: my-1-bucket-name, my-2-bucket-id" } ``` ### [Schema](#schema) All incoming webhooks will have event type set to `webhook` and attached `meta` field with additional information such as bucket ID, bucket name, input ID, input name: ``` { "type": "webhook", // event type "meta": { // bucket, input and output information "bucked_id": "1593fe5f-45f9-45cc-ba23-675fdc7c1638", "bucket_name": "my-1-bucket-name", "input_id": "b90f2fe9-621d-4290-9e74-edd5b61325dd", "input_name": "Default public endpoint", "output_name": "111", "output_destination": "http://localhost:8080" }, "headers": { // request headers "Content-Type": [ "application/json" ] }, "query": "foo=bar", // query (ie: /some-path?foo=bar) "body": "{\"hi\": \"there\"}", // request body "method": "PUT" // request method } ``` ## [JavaScript Example](#javascript-example) Here's a short example application written in JavaScript that subscribes to a stream of webhooks: ``` // client.js const WebSocket = require('ws'); var server = 'wss://my.webhookrelay.com/v1/socket'; var reconnectInterval = 1000 * 3; var ws; var apiKey = process.env.RELAY_KEY; var apiSecret = process.env.RELAY_SECRET; var connect = function(){ ws = new WebSocket(server); ws.on('open', function() { console.log('connected, sending authentication request'); ws.send(JSON.stringify({ action: 'auth', key: apiKey, secret: apiSecret })); }); ws.on('message', function incoming(data) { console.log(data) // parse message and if we have authenticated, subscribe to our bucket var msg = JSON.parse(data); if (msg.type === 'status' && msg.status === 'authenticated') { ws.send(JSON.stringify({ action: 'subscribe', buckets: ['my-bucket'] })); } }); ws.on('error', function() { console.log('socket error'); }); ws.on('close', function() { console.log('socket closed, reconnecting'); setTimeout(connect, reconnectInterval); }); }; connect(); ``` To run: #### [1. Install websocket library `ws`:](#_1-install-websocket-library-ws) ``` npm i ws ``` #### [2. Set token key and secret (from [tokens page](https://my.webhookrelay.com/tokens)):](#_2-set-token-key-and-secret-from-tokens-page) ``` export RELAY_KEY=your-token-key export RELAY_SECRET=your-token-secret ``` #### [3. Start it:](#_3-start-it) ``` node client.js ``` Now, if you send a webhook to your public input endpoint, you should see something similar: ``` $ node client.js {"type":"status","status":"authenticated","message":"connected successfully, subscribe to buckets"} {"type":"status","status":"subscribed","message":"subscribed to buckets: 123"} {"type":"webhook","meta":{"bucked_id":"89e44c32-27ff-4832-8655-8a42d3851b6f","bucket_name":"123","input_id":"ee4ac550-12a4-41a7-837d-dd3356ed1771","input_name":"Default public endpoint"},"headers":{"Content-Length":["15"],"User-Agent":["insomnia/6.0.2"],"Cookie":["__cfduid=dc244a014f0b1e2965544ddb483c3fe1b1525866866"],"Content-Type":["application/json"],"Accept":["*/*"]},"query":"","body":"{\"hi\": \"there\"}","method":"PUT"} ``` ## [JavaScript SDK example](#javascript-sdk-example) [Javascript library](https://www.npmjs.com/package/webhookrelay-ws-client) is available via `npm`. Library source code is available on [GitHub](https://github.com/webhookrelay/webhookrelay-ws-client). It's written in Typescript is a thin wrapper around our WebSocket server client. It can only subscribe to buckets but cannot create/update/delete them. To install it: ``` npm i webhookrelay-ws-client ``` ### [Library usage](#library-usage) ``` var ws = require(\`webhookrelay-ws-client\`); // handler function has to accept a JSON string and parse on its own var handler = function (data) { console.log(data) } // create a client with specified token key and secret from https://my.webhookrelay.com/tokens and any buckets that // can be created here https://my.webhookrelay.com/buckets. Handler function is called whenever there's a new message var client = new ws.WebhookRelayClient('your-token-key', 'your-token-secret', ['bucket-1', 'bucket-2'], handler) // connect starts a websocket connection to Webhook Relay client.connect(); ``` ### [Example application](#example-application) Set tokens as environment variables: ``` export RELAY_KEY=[YOUR TOKEN KEY] export RELAY_SECRET=[YOUR TOKEN SECRET] ``` ``` // app.js var ws = require(\`webhookrelay-ws-client\`); var apiKey = process.env.RELAY_KEY; var apiSecret = process.env.RELAY_SECRET; var handler = function (data) { console.log(data) } var run = function () { var client = new ws.WebhookRelayClient(apiKey, apiSecret, ['nodered'], handler) client.connect(); // do some work // disconnect whenever connection is no longer needed setTimeout(function(){ console.log('disconnecting') client.disconnect(); }, 10000); } run(); ``` To run it: ``` node app.js ``` Now, whenever you send webhooks to your public endpoint `https://my.webhookrelay.com/v1/webhooks/`, they will be received inside your application. You can subscribe to multiple buckets. Each message will have a JSON string that you can parse: ``` { "type": "webhook", // event type "meta": { // bucket, input and output information "bucked_id": "1593fe5f-45f9-45cc-ba23-675fdc7c1638", "bucket_name": "my-1-bucket-name", "input_id": "b90f2fe9-621d-4290-9e74-edd5b61325dd", "input_name": "Default public endpoint", "output_name": "111", "output_destination": "http://localhost:8080" }, "headers": { // request headers "Content-Type": [ "application/json" ] }, "query": "foo=bar", // query (ie: /some-path?foo=bar) "body": "{\"hi\": \"there\"}", // request body "method": "PUT" // request method } ``` Did this page help you? --- --- title: Polling webhooks with /v1/events | WebhookRelay meta: "og: title": "Polling webhooks with /v1/events" description: Poll webhook events with the Webhook Relay /v1/events API. Pull the oldest undelivered webhooks for a bucket and report delivery outcomes back. url: https://webhookrelay.com/docs/webhooks/polling-webhooks.md file: /docs/webhooks/polling-webhooks.md --- ![Stripes](https://webhookrelay.com/docs/webhooks/polling-webhooks/images/stripes.svg) Documentation **Fundamentals** # **Polling webhooks with /v1/events** Poll webhook events with the Webhook Relay /v1/events API. Pull the oldest undelivered webhooks for a bucket and report delivery outcomes back. ## [Overview](#overview) The `/v1/events` API lets your application **pull** webhook events from Webhook Relay instead of keeping a persistent WebSocket connection open. It is useful for workers, scheduled jobs, serverless consumers, back-office tools, and integrations that prefer a simple HTTP polling API. **Beta** Polling webhooks are currently a beta feature. If you run into unexpected behavior, please report it to [**support@webhookrelay.com**](https://webhookrelay.com/docs/webhooks/polling-webhooks//mailto:support@webhookrelay.com?Subject=Polling%20webhooks%20beta%20issue). Polling works as a **drain queue**, not a replayable stream. Each call returns the _oldest unsent_ webhooks for a bucket and marks them delivered, so the next call returns the next batch and the queue drains to an empty response once you are caught up. There is **no cursor to store** — the queue position is the webhook delivery status itself, so you simply keep calling the same URL. Use polling when: - You want to consume webhook events from an HTTP API rather than a WebSocket client. - Your consumer runs periodically, such as a cron job or serverless function. - You want a self-managing feed with no cursor or offset to persist. - You want to pull webhooks into your own queue, database, or event processor and acknowledge them by simply receiving them. Use [WebSockets](https://webhookrelay.com/docs/webhooks/polling-webhooks/docs/webhooks/websocket-server/) when your application needs a continuously connected, push-based stream. ## [How the drain queue works](#how-the-drain-queue-works) A webhook that arrives in a bucket starts in the `received` state. If the bucket has outputs, Webhook Relay delivers it to those destinations and the status moves to `sent` or `failed`. `/v1/events` is the **pull** alternative: it returns the webhooks that are still `received` — the ones nobody has delivered yet — and treats handing them to you as the delivery. Concretely, on every poll Webhook Relay: 1. Selects up to `limit` of the **oldest** webhooks still in the `received` state for your bucket. 2. Marks each one **delivered** (`received` → `sent`) before writing the response. The mark is synchronous, so the next poll will not return it again. 3. Returns the consumed webhooks in the `logs` array, oldest first. Because the pull _is_ the delivery, there is nothing to acknowledge in the happy path. If your consumer actually failed to process a webhook, you can correct the recorded outcome afterwards with [`PUT /v1/logs/{id}`](#reporting-a-different-outcome) — see below. ## [Endpoint](#endpoint) ``` GET https://my.webhookrelay.com/v1/events ``` Create an access token from the [tokens page](https://my.webhookrelay.com/tokens) and pass it as a bearer token. Tokens look like `sk-whrm-…`. ``` export RELAY_TOKEN=your-api-token export BUCKET=your-bucket-name-or-id ``` > The dashboard generates a ready-to-run snippet for you: open a bucket, choose **Connect Agent → Poll**, and copy the pre-filled `curl` / Node.js commands. Basic authentication with a token key and secret (`-u key:secret`) also works if your integration already uses it. ## [Pull events](#pull-events) Every request must include a `bucket` query parameter — there is no cursor to carry the scope. The reference can be the **bucket ID or its account-unique name**. Optionally pass `output` to drain webhooks for a single destination only. ``` curl -sS -H "Authorization: Bearer $RELAY_TOKEN" \ "https://my.webhookrelay.com/v1/events?bucket=$BUCKET&limit=1" ``` Example response: ``` { "logs": [ { "id": "9b31d6dc-6d14-4f83-90cb-0b402c02e3cc", "created_at": 1717243200, "updated_at": 1717243200, "bucket_id": "2cf96f7f-7a83-47f7-84d2-f5692e6f68c0", "input_id": "09a0a807-3f3f-4b66-af3b-f6f79e4f4d36", "output_id": "7df32a66-6465-4677-bf5c-f2cf9b8ffdb5", "status": "received", "method": "POST", "headers": { "Content-Type": ["application/json"] }, "raw_query": "source=stripe", "extra_path": "/checkout", "body": "{\"type\":\"checkout.session.completed\"}", "ip_address": "203.0.113.10" } ], "has_more": false } ``` The returned webhooks were in the `received` state — receiving them through `/v1/events` is what marks them delivered, so the same webhook is never handed out twice to a single consumer. When the queue is empty the response is simply `{ "logs": [], "has_more": false }`. ## [Continuous polling](#continuous-polling) Keep calling the same URL. When `has_more` is `true`, the page filled to the requested `limit` and more webhooks are waiting — poll again immediately. When it is `false`, back off before polling again. A delay of 2–10 seconds is a good starting point for most consumers. This Node.js example (Node 18+, no dependencies) drains the queue one webhook at a time and backs off when it is empty: ``` const ENDPOINT = 'https://my.webhookrelay.com/v1/events'; const BUCKET = process.env.BUCKET; // bucket name or ID const TOKEN = process.env.RELAY_TOKEN; // API token from /tokens async function poll() { // Pull the oldest unsent webhooks (limit 1, max 100). Each one // returned is marked delivered, so the next call returns the next // batch and eventually an empty list once you're caught up. const res = await fetch(\`${ENDPOINT}?bucket=${BUCKET}&limit=1\`, { headers: { Authorization: \`Bearer ${TOKEN}\` } }); if (!res.ok) { console.error(\`poll failed: ${res.status} ${await res.text()}\`); return setTimeout(poll, 5000); } const data = await res.json(); for (const log of data.logs) { console.log(log.id, log.method, log.extra_path); // Handle the webhook here. If you couldn't process it, report the // real outcome with PUT /v1/logs/{log.id} (status code + body). } // has_more === true => more waiting, fetch again straight away. setTimeout(poll, data.has_more ? 0 : 3000); } poll(); ``` ## [Query parameters](#query-parameters) | Parameter | Required | Description | | --- | --- | --- | | `bucket` | Required on every request | Bucket ID **or** account-unique name to drain. There is no cursor, so the bucket must be supplied each call. | | `output` | Optional | Output ID filter. Use it when one consumer should drain events for a single destination. | | `limit` | Optional | Page size. Defaults to `1`; maximum is `100`. Each returned webhook is marked delivered, so a larger limit consumes a larger batch per poll. | | `max_age` | Optional | Lookback window for unsent webhooks, using Go duration syntax such as `1h`, `24h`, or `168h`. Defaults to `24h`. Send a larger value if a consumer may be offline long enough that older webhooks must still be drained. | ## [Response fields](#response-fields) | Field | Description | | --- | --- | | `logs` | Array of webhook records, oldest first. Each one was just marked delivered. Empty once the queue is drained. | | `has_more` | `true` when the page filled to `limit` and at least one webhook was delivered — poll again immediately. `false` means back off. | Common `logs` fields: | Field | Description | | --- | --- | | `id` | Webhook log ID. Use it for de-duplication and to report a different outcome with `PUT /v1/logs/{id}`. | | `created_at` | Unix timestamp when Webhook Relay received the webhook. | | `updated_at` | Unix timestamp for the latest delivery update. | | `bucket_id`, `input_id`, `output_id` | IDs for the bucket, public input endpoint, and destination output. | | `status` | Delivery status. Webhooks returned by `/v1/events` were in `received` — pulling them is the delivery. | | `method`, `headers`, `raw_query`, `extra_path`, `body`, `ip_address` | Original webhook request data. | | `status_code`, `duration_ms`, `retries` | Destination response status, delivery duration, and retry count — present only when a delivery to an output was attempted. | | `ephemeral` | `true` when the bucket is configured not to store request details. | ## [Delivery semantics](#delivery-semantics) - **Single consumer → at-most-once.** Each webhook is handed out exactly once, then marked delivered, so one poller never sees the same webhook twice. - **Concurrent consumers → at-least-once.** There is no per-record lease, so two clients polling the same bucket at the same moment can both receive the same webhook (the second mark is a harmless no-op). De-duplicate by webhook `id` if a webhook can trigger a non-idempotent side effect. - **Transient failures delay, not drop.** If Webhook Relay cannot mark a webhook delivered (a rare storage write error), it leaves it `received` and returns it again on a later poll. A webhook is never silently lost. ## [Reporting a different outcome](#reporting-a-different-outcome) Receiving a webhook through `/v1/events` records it as delivered (`sent`). If your consumer could not process it, report the real outcome with `PUT /v1/logs/{id}`, supplying the destination `status_code` (and optionally a `status` and `response_body`): ``` curl -sS -X PUT -H "Authorization: Bearer $RELAY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"status_code": 500}' \ "https://my.webhookrelay.com/v1/logs/9b31d6dc-6d14-4f83-90cb-0b402c02e3cc" ``` A webhook log is only editable for a short window after it was received (about a minute), so report the outcome promptly after pulling it. ## [Error handling](#error-handling) - `400 Bad Request` — the `bucket` query parameter is missing. - `401 Unauthorized` — the access token or Basic auth credentials are missing or invalid. - `404 Not Found` — the bucket does not exist or does not belong to the authenticated account. - `408 Request Timeout` — the request was canceled before the page could be consumed; retry. - `429 Too Many Requests` — the account rate limit was reached. Back off before retrying. - `500 Internal Server Error` — a transient read error. Retry with backoff. - `503 Service Unavailable` — polling is not enabled on that deployment. Use [WebSockets](https://webhookrelay.com/docs/webhooks/polling-webhooks/docs/webhooks/websocket-server/) or the webhook logs API as a fallback. ## [Polling checklist](#polling-checklist) - Send `bucket` (ID or name) on every request, plus an optional `output`. - Poll immediately while `has_more` is `true`; back off when it is `false`. - Treat receiving a webhook as the acknowledgement — there is no cursor to persist. - Report a different outcome with `PUT /v1/logs/{id}` if your consumer failed to process a webhook. - De-duplicate by log `id` if you run more than one consumer on the same bucket or trigger non-idempotent side effects. Did this page help you? --- --- title: Static IP Address | WebhookRelay meta: "og: title": "Static IP Address" description: Enable a static IP address for outgoing webhooks to allow IP whitelisting. url: https://webhookrelay.com/docs/webhooks/static-ip-address.md file: /docs/webhooks/static-ip-address.md --- ![Stripes](https://webhookrelay.com/docs/webhooks/static-ip-address/images/stripes.svg) Documentation **Fundamentals** # **Static IP Address** Enable a static IP address for outgoing webhooks to allow IP whitelisting. ## [Overview](#overview) Webhook Relay provides a static outgoing IP address (**5.161.20.156**) for outgoing webhook requests. This allows you to whitelist this specific IP address in your receiving infrastructure, enhancing security and reliability. ## [Why use a static IP?](#why-use-a-static-ip) - **Enhanced Security:** Restrict incoming connections to only known, trusted IP addresses. - **Simplified Configuration:** Avoid the need to update IP whitelists if the underlying infrastructure changes. - **Service Compatibility:** Some services require or recommend using static IPs for integrations. ## [Enabling static IP](#enabling-static-ip) To enable the static IP feature for your webhooks: 1. Navigate to your [Buckets page](https://my.webhookrelay.com/buckets). 2. Either create a new Bucket or edit an existing one. 3. In the Bucket settings page, locate the **Static IP** option and toggle it on. ![Enable Static IP in Bucket settings](https://webhookrelay.com/docs/webhooks/static-ip-address/images/blog/static-ip/config.png) Once enabled, all webhook forwards configured within that Bucket will originate from the IP address `5.161.20.156`. ## [Important Considerations](#important-considerations) - The static IP feature is configured on a per-Bucket basis. - If integrating with a third-party service, consult their documentation for specific instructions on whitelisting IP addresses. Did this page help you? --- --- title: CORS for webhooks | WebhookRelay meta: "og: title": "CORS for webhooks" description: Configure CORS for your webhooks to allow requests from other domains. url: https://webhookrelay.com/docs/webhooks/cors.md file: /docs/webhooks/cors.md --- ![Stripes](https://webhookrelay.com/docs/webhooks/cors/images/stripes.svg) Documentation **Fundamentals** # **CORS for webhooks** Configure CORS for your webhooks to allow requests from other domains. ## [What is CORS?](#what-is-cors) CORS is a security feature that allows web applications to make requests to a different domain than the one that served the web page. It is a way to allow web applications to access resources from different domains. ## [How to configure CORS for your webhooks](#how-to-configure-cors-for-your-webhooks) Go to your bucket details and click on "Response Configuration" next to the input: ![Custom Domains](https://webhookrelay.com/docs/webhooks/cors/images/docs/webhooks/cors/response-config.png) Then, in the headers section add the following headers: - `Access-Control-Allow-Origin: *` - `Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS` - `Access-Control-Allow-Headers: Content-Type, Authorization` Then, click on "Save" button to apply the changes. ## [That's it!](#thats-it) That's it! You can now configure CORS for your webhooks to allow requests from other domains. ## [Alternative Option - Function](#alternative-option-function) You can also [create a function](https://my.webhookrelay.com/functions) with the following code: ``` r:SetResponseStatusCode(200) r:SetResponseHeader("Access-Control-Allow-Origin", "*") r:SetResponseHeader("Access-Control-Allow-Methods", "POST") r:SetResponseHeader("Access-Control-Allow-Headers", "content-type") ``` Once created, you need to add this to your input. Did this page help you? --- --- title: Custom response to webhooks | WebhookRelay meta: "og: title": "Custom response to webhooks" description: Configure a custom response to your webhooks, some applications require it, for example Facebook webhooks. url: https://webhookrelay.com/docs/webhooks/custom-webhook-response.md file: /docs/webhooks/custom-webhook-response.md --- ![Stripes](https://webhookrelay.com/docs/webhooks/custom-webhook-response/images/stripes.svg) Documentation **Fundamentals** # **Custom response to webhooks** Configure a custom response to your webhooks, some applications require it, for example Facebook webhooks. ## [Returning custom response to webhooks](#returning-custom-response-to-webhooks) Some applications expect a response when they send a webhook. This can be a special header, body or status code. ## [Configuring input](#configuring-input) Go to your bucket details and click on "Response Configuration" next to the input: ![Custom Domains](https://webhookrelay.com/docs/webhooks/custom-webhook-response/images/docs/webhooks/cors/response-config.png) There, you can configure: - Choose - dynamic response from output, this will return a response from any output that responds within 10 seconds. Your server can return any payload here. - Static response - you can response status code, body and headers. Once selected, click "Save" ## [Alternative Option - Function](#alternative-option-function) You can also [create a function](https://my.webhookrelay.com/functions) to return a custom response to a webhook. Go to your functions page and click on "Create Function" button. ``` r:SetResponseStatusCode(200) r:SetResponseHeader("Content-Type", "application/json") r:SetResponseBody("{\"message\":\"Webhook received\"}") ``` This function needs to be attached to the input. Functions that are attached to the output cannot modify the response. Did this page help you? --- --- title: Zero Retention Buckets | WebhookRelay meta: "og: title": "Zero Retention Buckets" description: Stop storing webhook payloads and headers while keeping a record that each request happened. url: https://webhookrelay.com/docs/webhooks/zero-retention-buckets.md file: /docs/webhooks/zero-retention-buckets.md --- ![Stripes](https://webhookrelay.com/docs/webhooks/zero-retention-buckets/images/stripes.svg) Documentation **Fundamentals** # **Zero Retention Buckets** Stop storing webhook payloads and headers while keeping a record that each request happened. ## [What is a zero retention bucket?](#what-is-a-zero-retention-bucket) Enable the **Ephemeral** setting on a bucket when you need to process webhooks without retaining their contents. Webhook Relay records that a request happened, but does not persist the request body, request headers, response body, or response headers. This can help reduce the amount of webhook data stored by Webhook Relay when you only need the bucket to receive and forward requests in real time. ## [Enable zero retention](#enable-zero-retention) 1. Open the [Buckets page](https://my.webhookrelay.com/buckets). 2. Open the bucket you want to configure. 3. Select the **Settings** tab. 4. Turn on **Ephemeral**. 5. Click **Save**. ![Bucket settings with Ephemeral enabled](https://webhookrelay.com/docs/webhooks/zero-retention-buckets/images/docs/webhooks/ephemeral_bucket.png) Once enabled, the bucket keeps only a record that the request occurred. Its request and response contents are not retained for later inspection. ## [Important: retries are not available](#important-retries-are-not-available) Because an ephemeral bucket does not persist the request or response data, Webhook Relay cannot retry a request after the initial delivery attempt. Enable **Ephemeral** only when you do not need retry support or access to the request contents after delivery. Did this page help you? --- --- title: Large Webhooks | WebhookRelay meta: "og: title": "Large Webhooks" description: Forward large webhook payloads over the default 4 MB limit. Enable support for webhooks up to 40 MB, or contact us for limits of 100 MB and beyond. url: https://webhookrelay.com/docs/webhooks/large-webhooks.md file: /docs/webhooks/large-webhooks.md --- ![Stripes](https://webhookrelay.com/docs/webhooks/large-webhooks/images/stripes.svg) Documentation **Fundamentals** # **Large Webhooks** Forward large webhook payloads over the default 4 MB limit. Enable support for webhooks up to 40 MB, or contact us for limits of 100 MB and beyond. ## [Forward webhooks larger than 4 MB](#forward-webhooks-larger-than-4-mb) Webhook Relay buckets have a default webhook size limit of **4 MB**. If a provider sends larger payloads, enable **Large webhooks** on the bucket to receive and forward webhook requests up to **40 MB**. This is useful for providers that include large JSON payloads, documents, images, or other files in webhook requests. ## [Enable large webhooks](#enable-large-webhooks) 1. Open the [Buckets page](https://my.webhookrelay.com/buckets). 2. Open the bucket you want to configure. 3. Select the **Settings** tab. 4. Turn on **Large webhooks**. 5. Click **Save**. ![Bucket settings with Large webhooks enabled](https://webhookrelay.com/docs/webhooks/large-webhooks/images/docs/webhooks/large_webhooks.png) Once enabled, the bucket can accept and forward webhook requests up to 40 MB. ## [Need to forward webhooks larger than 40 MB?](#need-to-forward-webhooks-larger-than-40-mb) Webhook size limits can be raised to **100 MB or beyond**. [Contact support](https://webhookrelay.com/docs/webhooks/large-webhooks/contact/) with your expected payload size and use case so we can configure the right limit for your account. Did this page help you? --- --- title: Alerts | WebhookRelay meta: "og: title": Alerts description: Get notified when webhook delivery fails. Define alert policies, choose destinations (Email, Slack, Discord, Teams, Telegram, Pushover, SMTP or webhook), and Webhook Relay opens and resolves incidents automatically. url: https://webhookrelay.com/docs/webhooks/alerts.md file: /docs/webhooks/alerts.md --- ![Stripes](https://webhookrelay.com/docs/webhooks/alerts/images/stripes.svg) Documentation **Fundamentals** # **Alerts** Get notified when webhook delivery fails. Define alert policies, choose destinations (Email, Slack, Discord, Teams, Telegram, Pushover, SMTP or webhook), and Webhook Relay opens and resolves incidents automatically. ## [Get notified when webhook delivery fails](#get-notified-when-webhook-delivery-fails) **Alerts** watch your buckets for delivery problems. You define a policy (what counts as a failure and when to open an incident), pick where notifications go, and select which buckets to monitor. When delivery outcomes cross the policy, Webhook Relay **opens an incident** and sends a notification. When delivery recovers, the incident **resolves automatically**. For a short product overview, see the [Alerts feature page](https://webhookrelay.com/docs/webhooks/alerts/features/alerts/). For custom logic (e.g. alert only when a payload field is wrong), use [alerting from functions](https://webhookrelay.com/docs/webhooks/alerts/docs/webhooks/functions/alerting/) instead. ## [Create an alert](#create-an-alert) 1. Open **Alerts** in the Webhook Relay dashboard ([my.webhookrelay.com](https://my.webhookrelay.com)). 2. Click **New alert policy** (or **Create alert**). 3. Configure the three parts of the form: - **Destination** — where incidents should go - **Alert policy** — what to monitor and when to open - **Buckets** — which buckets this policy watches 4. Click **Create alert**. ![Create alert dialog — destination, alert policy for delivery failures, and bucket selection](https://webhookrelay.com/docs/webhooks/alerts/images/docs/webhooks/alerts/create-alert.png) ## [Destination — where should incidents go?](#destination-where-should-incidents-go) Pick a notification channel and enter its credentials. Fields are validated before saving. ![Alert destination types: Email, Slack, Discord, Telegram, Microsoft Teams, Pushover, SMTP email, Webhook](https://webhookrelay.com/docs/webhooks/alerts/images/docs/webhooks/alerts/destinations.png) Supported destination types: | Destination | Use it for | | --- | --- | | **Email** | Inbox notifications | | **Slack** | Channel posts for eng / on-call | | **Discord** | Server / channel webhooks | | **Telegram** | Bot messages | | **Microsoft Teams** | Channel notifications | | **Pushover** | Mobile push for on-call | | **SMTP email** | Your own mail server | | **Webhook** | Any HTTPS endpoint (PagerDuty, Opsgenie, custom bots, …) | Give the alert a name so you can tell policies apart in the list. You can create multiple alerts with different destinations for the same buckets if you want fan-out (e.g. Slack for the team and a webhook into your incident tool). ## [Alert policy — when should an incident open?](#alert-policy-when-should-an-incident-open) ![Alert policy: delivery failures, any failed delivery or error rate threshold, rolling window, and recovery behavior](https://webhookrelay.com/docs/webhooks/alerts/images/docs/webhooks/alerts/alert-policy.png) ### [Alert type](#alert-type) Currently supported: - **Delivery failures** — fire when deliveries to a watched bucket fail (errors, timeouts, unreachable destinations). ### [Description (optional)](#description-optional) Free-text note included in **incident** and **recovery** notifications — e.g. `production stripe payments` — so on-call knows which system is affected. ### [Incident condition](#incident-condition) Choose how sensitive the policy is: | Condition | Behavior | | --- | --- | | **Any failed delivery** | Open on the first failure. | | **Error rate threshold** | Wait for a representative sample before opening (reduces noise from one-off blips). | ### [Rolling window](#rolling-window) The recovery window for the policy (e.g. **5 minutes**). With **Any failed delivery**: - **One failure** opens the incident. - The incident **resolves** after a complete rolling window with **no failures** and **at least one successful delivery**. - **Each bucket is evaluated independently** — failures in two buckets open two incidents. ## [Buckets](#buckets) Use the **Buckets** panel on the right to select which buckets this alert watches. Search and filter as needed, then toggle each bucket on. Only selected buckets are evaluated. Leave a bucket off if you do not want delivery noise from it to open incidents. ## [Incidents](#incidents) When a policy condition is met on a watched bucket, Webhook Relay opens an **incident**. Incidents are the live record of delivery problems — one per bucket per policy — and appear on the Alerts page with status, failure snapshot, and open/resolved times. ![Alerts page showing open incidents, incident chart over time, and a table of status, bucket, policy, failure snapshot, opened and resolved](https://webhookrelay.com/docs/webhooks/alerts/images/docs/webhooks/alerts/incidents.jpg) ### [When an incident starts](#when-an-incident-starts) An incident **opens** as soon as the alert policy’s condition is met for that bucket, for example: - **Any failed delivery** — first failed delivery - **Error rate threshold** — after a representative sample exceeds the threshold Each bucket is evaluated on its own, so two failing buckets under the same policy produce two incidents. Destinations on that alert receive an **incident opened** notification. ### [How an incident closes](#how-an-incident-closes) An incident can end in either way: 1. **Automatically** — after a full rolling window with **no failures** and **at least one successful delivery**, the incident resolves and a recovery notification is sent. 2. **Manually** — resolve the incident from the Alerts UI (row actions) when you have fixed the issue and do not want to wait for the recovery window. ### [History and filtering](#history-and-filtering) The Alerts page keeps incident history so you can always revisit what happened: - **Chart** — open-incident volume over 7 / 30 / 90 days - **Filters** — Policies, Open, Resolved, or All - **Table** — Status, Bucket (and output), Policy (condition + window), Failure snapshot (counts and error rate), Opened, Resolved Pair incidents with [webhook logs & monitoring](https://webhookrelay.com/docs/webhooks/alerts/features/webhook-logs/) to inspect the exact failing request, and with [durable retries](https://webhookrelay.com/docs/webhooks/alerts/docs/webhooks/durable-webhooks/) so failed deliveries keep retrying while you fix the destination. ## [Related](#related) - [Alerts feature page](https://webhookrelay.com/docs/webhooks/alerts/features/alerts/) - [Webhook logs & delivery monitoring](https://webhookrelay.com/docs/webhooks/alerts/features/webhook-logs/) - [Durable webhooks](https://webhookrelay.com/docs/webhooks/alerts/docs/webhooks/durable-webhooks/) - [Alerting from functions](https://webhookrelay.com/docs/webhooks/alerts/docs/webhooks/functions/alerting/) — custom request/response checks - [Audit logs](https://webhookrelay.com/docs/webhooks/alerts/features/audit-logs/) — who created or changed alert policies Did this page help you? --- --- title: Service Connections | WebhookRelay meta: "og: title": "Service Connections" description: Connect Webhook Relay to AWS and GCP cloud services. Receive events from S3, SQS, SNS, GCS, Pub/Sub and send webhooks to cloud providers. url: https://webhookrelay.com/docs/service-connections.md file: /docs/service-connections.md --- ![Stripes](https://webhookrelay.com/docs/service-connections/images/stripes.svg) Documentation **Fundamentals** # **Service Connections** Connect Webhook Relay to AWS and GCP cloud services. Receive events from S3, SQS, SNS, GCS, Pub/Sub and send webhooks to cloud providers. Service connections let you integrate Webhook Relay with cloud providers like **AWS**, **GCP** and **Azure**. Once connected, you can receive events from cloud services (inputs) and send webhooks into cloud services (outputs) — all through your existing buckets. ![Service Connections](https://webhookrelay.com/docs/service-connections/images/docs/sc/add_sc.png) ## [How It Works](#how-it-works) A service connection stores your cloud provider credentials (encrypted at rest). You then attach **inputs** and **outputs** to any bucket: - **Inputs** poll or subscribe to cloud services and relay messages into your bucket as webhook events - **Outputs** forward incoming webhooks from your bucket to cloud services ``` Cloud Service ──► Input ──► Bucket ──► Output ──► Cloud Service (SQS, Pub/Sub) (S3, SNS) ``` This means you can bridge different cloud providers through Webhook Relay. For example, receive messages from a GCP Pub/Sub topic and forward them to an AWS SQS queue — or vice versa. ## [Supported Services](#supported-services) | Provider | Service | Receive Events (Input) | Send Webhooks (Output) | | --- | --- | :---: | :---: | | AWS | [S3](https://webhookrelay.com/docs/service-connections/docs/service-connections/aws_s3/) | Object notifications | Upload objects | | AWS | [SQS](https://webhookrelay.com/docs/service-connections/docs/service-connections/aws_sqs/) | Poll messages | Send messages | | AWS | [SNS](https://webhookrelay.com/docs/service-connections/docs/service-connections/aws_sns/) | Subscribe to topics | Publish to topics | | GCP | [Cloud Storage (GCS)](https://webhookrelay.com/docs/service-connections/docs/service-connections/gcp_gcs/) | Object notifications | Upload objects | | GCP | [Pub/Sub](https://webhookrelay.com/docs/service-connections/docs/service-connections/gcp_pubsub/) | Subscribe to topics | Publish to topics | | Azure | [Cosmos DB, Blob Storage…](https://webhookrelay.com/docs/service-connections/docs/service-connections/azure/) | In progress | In progress | [Azure](https://webhookrelay.com/docs/service-connections/docs/service-connections/azure/) (Cosmos DB, Blob Storage, Service Bus) is in progress — [contact us](https://webhookrelay.com/docs/service-connections/contact/) to enable it early for your account. Need something else? Ping us at [info@webhookrelay.com](https://webhookrelay.com/docs/service-connections//mailto:info@webhookrelay.com) and we'll add it to our roadmap. ## [Setting Up Credentials](#setting-up-credentials) ![Service Connections](https://webhookrelay.com/docs/service-connections/images/docs/sc/service_connections.png) Service connections can be added [here](https://my.webhookrelay.com/service-connections). ### [AWS](#aws) You need an **Access Key ID** and **Secret Access Key** from an IAM user with permissions for the services you want to use. 1. Go to **AWS IAM Console** > Users > select or create a user 2. Under **Security Credentials**, create an Access Key 3. Copy the Access Key ID and Secret Access Key 4. Create a service connection in Webhook Relay with these credentials ### [GCP](#gcp) You need a **Service Account JSON key** from a GCP project. 1. Go to **GCP Console** > IAM & Admin > Service Accounts 2. Create a service account (or use existing) and grant roles for the services you need 3. Go to **Keys** tab > Add Key > Create New Key > JSON 4. Create a service connection in Webhook Relay, paste the JSON key contents ## [Transforming Messages with Functions](#transforming-messages-with-functions) You can attach [Functions](https://webhookrelay.com/docs/service-connections/docs/webhooks/functions/) to your bucket to transform messages as they pass through. This is useful when bridging different services that expect different payload formats. For example, you could receive an S3 object notification, use a function to extract the relevant data and reformat it, then forward the result to a Pub/Sub topic or any HTTPS endpoint. See the [JSON encoding](https://webhookrelay.com/docs/service-connections/docs/webhooks/functions/manipulating-json/) and [HTTP requests](https://webhookrelay.com/docs/service-connections/docs/webhooks/functions/make-http-request/) guides for details on payload manipulation. ## [Cross-Cloud and Hybrid Examples](#cross-cloud-and-hybrid-examples) Because Webhook Relay acts as a broker between inputs and outputs, you can combine any input with any output — even across providers: | Use Case | Input | Output | | --- | --- | --- | | Bridge GCP to AWS | GCP Pub/Sub subscription | AWS SQS queue | | Bridge AWS to GCP | AWS SNS topic | GCP Pub/Sub topic | | Cloud to on-premises | AWS SQS queue | [Internal destination](https://webhookrelay.com/docs/service-connections/docs/webhooks/internal/localhost/) (localhost) | | Cloud to any API | GCP GCS bucket | [Public destination](https://webhookrelay.com/docs/service-connections/docs/webhooks/public/public-destination/) (any HTTPS endpoint) | | Multi-cloud fan-out | AWS S3 notifications | GCP GCS + AWS SQS + HTTPS API | ## [Security](#security) - **Encryption at rest** — secret fields (Secret Access Key, Service Account Key) are encrypted with AES-256-GCM - **Credential masking** — API responses never return full credentials - **Account isolation** — each account can only access its own connections Did this page help you? --- --- title: AWS S3 | WebhookRelay meta: "og: title": "AWS S3" description: Receive S3 object notifications as webhooks and upload webhook data to S3 buckets using Webhook Relay service connections. url: https://webhookrelay.com/docs/service-connections/aws_s3.md file: /docs/service-connections/aws_s3.md --- ![Stripes](https://webhookrelay.com/docs/service-connections/aws_s3/images/stripes.svg) Documentation **Fundamentals** # **AWS S3** Receive S3 object notifications as webhooks and upload webhook data to S3 buckets using Webhook Relay service connections. Connect Webhook Relay to **Amazon S3** to store incoming webhook data as S3 objects (output). ## [Prerequisites](#prerequisites) - An [AWS service connection](https://webhookrelay.com/docs/service-connections/aws_s3/docs/service-connections/) with credentials that have S3 permissions - An S3 bucket in your AWS account ### [IAM Permissions](#iam-permissions) **For S3 Output (upload objects):** - `s3:PutObject` ## [S3 Output — Upload Webhook Data to S3](#s3-output-upload-webhook-data-to-s3) S3 outputs store incoming webhook data as objects in your S3 bucket. Each webhook is saved as a separate file. ### [Configuration](#configuration) | Field | Required | Description | | --- | :---: | --- | | `bucket_name` | Yes | S3 bucket name | | `region` | Yes | AWS region | | `prefix` | No | Key prefix for uploaded objects (e.g. `webhooks/`) | | `file_format` | No | Storage format: `json` (default), `body_only`, `har` | ### [Object Path](#object-path) Objects are stored with a date-based path: ``` {prefix}/{year}/{month}/{day}/{log_id}.json ``` For example: `webhooks/2026/02/24/whl_abc123.json` ## [Example: Bridge GCP Pub/Sub to AWS S3](#example-bridge-gcp-pubsub-to-aws-s3) You can receive messages from a GCP Pub/Sub subscription and archive them as objects in an S3 bucket. This is useful for cross-cloud data archival: 1. Create a [GCP service connection](https://webhookrelay.com/docs/service-connections/aws_s3/docs/service-connections/) with Pub/Sub subscriber permissions 2. Create an [AWS service connection](https://webhookrelay.com/docs/service-connections/aws_s3/docs/service-connections/) with S3 write permissions 3. Create a bucket in Webhook Relay 4. Add a **GCP Pub/Sub input** on the bucket (messages flow in) 5. Add an **AWS S3 output** on the bucket (messages get stored as objects) Every message published to your Pub/Sub topic will automatically be archived as an S3 object. ### [Transform Before Storing](#transform-before-storing) Attach a [Function](https://webhookrelay.com/docs/service-connections/aws_s3/docs/webhooks/functions/) to the bucket to transform the payload before it reaches S3. For example, extract only the relevant fields from a Pub/Sub message: ``` const message = JSON.parse(r.body) // Extract just the data you need const simplified = { event_type: message.attributes.event_type, data: message.data, timestamp: message.publish_time } r.setBody(JSON.stringify(simplified)) ``` See the [JSON encoding](https://webhookrelay.com/docs/service-connections/aws_s3/docs/webhooks/functions/manipulating-json/) guide for more transformation examples. Did this page help you? --- --- title: AWS SNS | WebhookRelay meta: "og: title": "AWS SNS" description: Subscribe to Amazon SNS topics and publish webhook data to SNS using Webhook Relay service connections. url: https://webhookrelay.com/docs/service-connections/aws_sns.md file: /docs/service-connections/aws_sns.md --- ![Stripes](https://webhookrelay.com/docs/service-connections/aws_sns/images/stripes.svg) Documentation **Fundamentals** # **AWS SNS** Subscribe to Amazon SNS topics and publish webhook data to SNS using Webhook Relay service connections. Connect Webhook Relay to **Amazon SNS** to subscribe to topics (input) or publish webhook data to topics (output). ## [Prerequisites](#prerequisites) - An [AWS service connection](https://webhookrelay.com/docs/service-connections/aws_sns/docs/service-connections/) with credentials that have SNS and SQS permissions - An SNS topic in your AWS account ### [IAM Permissions](#iam-permissions) **For SNS Input (subscribe to topics):** - `sns:Subscribe` - `sns:GetTopicAttributes` - `sqs:CreateQueue` - `sqs:SetQueueAttributes` - `sqs:ReceiveMessage` - `sqs:DeleteMessage` - `sqs:GetQueueUrl` > SNS inputs require SQS permissions because Webhook Relay creates a dedicated SQS queue (`whr-sns-{input_id}`) and subscribes it to your SNS topic. Messages are then polled from this queue. **For SNS Output (publish to topics):** - `sns:Publish` - `sns:GetTopicAttributes` ## [SNS Input — Subscribe to a Topic](#sns-input-subscribe-to-a-topic) ![Service Connection Input](https://webhookrelay.com/docs/service-connections/aws_sns/images/docs/sc/add_input.png) SNS inputs subscribe to your SNS topic and relay every published message into your Webhook Relay bucket. Internally, a dedicated SQS queue is created and subscribed to the topic, then polled for messages. ### [Configuration](#configuration) | Field | Required | Description | | --- | :---: | --- | | `topic_arn` | Yes | SNS Topic ARN (e.g. `arn:aws:sns:us-east-1:123456789:my-topic`) | | `region` | No | AWS region — auto-extracted from the ARN | | `subscription_arn` | Auto | Populated after the subscription is created | **ARN format:** `arn:aws[-cn|-us-gov]:sns:::` ## [SNS Output — Publish Webhooks to a Topic](#sns-output-publish-webhooks-to-a-topic) SNS outputs publish incoming webhook data as messages to your SNS topic. This lets you fan out webhooks to all SNS subscribers (Lambda, SQS, email, HTTP endpoints, etc.). ### [Configuration](#configuration-1) | Field | Required | Description | | --- | :---: | --- | | `topic_arn` | Yes | SNS Topic ARN | | `region` | No | AWS region — auto-extracted from the ARN | ## [Example: Bridge GCP Pub/Sub to AWS SNS](#example-bridge-gcp-pubsub-to-aws-sns) Route messages from a GCP Pub/Sub subscription to your SNS topic — useful for triggering Lambda functions, sending notifications, or fanning out to multiple SQS queues: 1. Create a [GCP service connection](https://webhookrelay.com/docs/service-connections/aws_sns/docs/service-connections/) with Pub/Sub subscriber permissions 2. Create an [AWS service connection](https://webhookrelay.com/docs/service-connections/aws_sns/docs/service-connections/) with SNS publish permissions 3. Create a bucket in Webhook Relay 4. Add a **[GCP Pub/Sub](https://webhookrelay.com/docs/service-connections/aws_sns/docs/service-connections/gcp_pubsub/) input** on the bucket (messages flow in) 5. Add an **AWS SNS output** on the bucket (messages published to the topic) ### [Transform the Message](#transform-the-message) Use a [Function](https://webhookrelay.com/docs/service-connections/aws_sns/docs/webhooks/functions/) to reshape the Pub/Sub message into a format your SNS subscribers expect: ``` const pubsubMessage = JSON.parse(r.body) // Create a message your SNS subscribers can process const notification = { source: "gcp-pubsub", topic: pubsubMessage.subscription, data: pubsubMessage.data, attributes: pubsubMessage.attributes, timestamp: new Date().toISOString() } r.setBody(JSON.stringify(notification)) ``` See the [JSON encoding](https://webhookrelay.com/docs/service-connections/aws_sns/docs/webhooks/functions/manipulating-json/) and [HTTP requests](https://webhookrelay.com/docs/service-connections/aws_sns/docs/webhooks/functions/make-http-request/) guides for more function examples. ## [Example: SNS to External HTTPS Webhook](#example-sns-to-external-https-webhook) While SNS natively supports HTTP/HTTPS subscriptions, Webhook Relay adds capabilities on top: - **Payload transformation** — reshape the SNS message before delivery using [Functions](https://webhookrelay.com/docs/service-connections/aws_sns/docs/webhooks/functions/) - **Authentication** — add [custom auth headers](https://webhookrelay.com/docs/service-connections/aws_sns/docs/webhooks/auth/username-password/) to outgoing requests - **Retry and logging** — full delivery logs with automatic retries - **Multi-destination** — forward to multiple HTTPS endpoints, [SQS queues](https://webhookrelay.com/docs/service-connections/aws_sns/docs/service-connections/aws_sqs/), [S3](https://webhookrelay.com/docs/service-connections/aws_sns/docs/service-connections/aws_s3/), or GCP services simultaneously 1. Create an AWS service connection 2. Create a bucket with an **SNS input** pointing to your topic 3. Add one or more [public destinations](https://webhookrelay.com/docs/service-connections/aws_sns/docs/webhooks/public/public-destination/) ## [Example: AWS SNS to GCP Pub/Sub](#example-aws-sns-to-gcp-pubsub) Route SNS messages to a GCP Pub/Sub topic for cross-cloud event distribution: 1. Create an AWS service connection with SNS subscribe permissions 2. Create a [GCP service connection](https://webhookrelay.com/docs/service-connections/aws_sns/docs/service-connections/) with Pub/Sub publisher permissions 3. Create a bucket with an **SNS input** and a **[GCP Pub/Sub](https://webhookrelay.com/docs/service-connections/aws_sns/docs/service-connections/gcp_pubsub/) output** Every message published to your SNS topic is automatically forwarded to the Pub/Sub topic, bridging AWS and GCP event systems. Did this page help you? --- --- title: Email webhook payload | WebhookRelay meta: "og: title": "Email webhook payload" description: Reference for the JSON payload Webhook Relay delivers for inbound email: from, recipient, subject, text, html, headers, SPF/DKIM/DMARC and attachments. url: https://webhookrelay.com/docs/email/payload.md file: /docs/email/payload.md --- ![Stripes](https://webhookrelay.com/docs/email/payload/images/stripes.svg) Documentation **Fundamentals** # **Email webhook payload** Reference for the JSON payload Webhook Relay delivers for inbound email: from, recipient, subject, text, html, headers, SPF/DKIM/DMARC and attachments. Each inbound email is parsed server-side and delivered as a JSON `POST` with `Content-Type: application/json` and an `X-Webhookrelay-Source: email` header. Empty fields are omitted, so a plain-text email with no attachments won't include `html`, `cc` or `attachments`. ## [Example (HTML email with an attachment)](#example-html-email-with-an-attachment) ``` { "from": "jon@example.com", "from_name": "Jon Snow", "recipient": "5519fb0d-997a-46cd-b102-ac990b4c38fa@in.webhookrelay-mail.com", "to": ["5519fb0d-997a-46cd-b102-ac990b4c38fa@in.webhookrelay-mail.com"], "cc": ["team@example.com"], "subject": "Invoice #1042", "date": "Fri, 26 Jun 2026 11:27:41 +0400", "message_id": "C70BE3E7-4B20-4240-A1B8-2D19E0032FFD@gmail.com", "text": "Hi, your invoice is attached.\r\n", "html": "

Hi, your invoice is attached.

", "headers": { "From": "Jon Snow ", "To": "5519fb0d-997a-46cd-b102-ac990b4c38fa@in.webhookrelay-mail.com", "Subject": "Invoice #1042", "Content-Type": "multipart/mixed; boundary=...", "Return-Path": "" }, "spf": "pass", "dkim": "pass", "dmarc": "pass", "attachments": [ { "name": "invoice.pdf", "content_type": "application/pdf", "size": 48213, "content": "JVBERi0xLjQKJ..." } ] } ``` ## [Fields](#fields) | Field | Type | Notes | | --- | --- | --- | | `from` | string | Sender address, lower-cased (e.g. `jon@example.com`). | | `from_name` | string | Sender display name, if present (e.g. `Jon Snow`). Omitted when empty. | | `recipient` | string | The inbound address that matched — your email input's `@in.webhookrelay-mail.com`. | | `to` | string | All `To` addresses. | | `cc` | string | All `Cc` addresses. Omitted when empty. | | `subject` | string | The email subject. | | `date` | string | The `Date` header, as sent (RFC 2822, e.g. `Fri, 26 Jun 2026 11:27:41 +0400`). | | `message_id` | string | The `Message-ID`, useful for de-duplication. | | `text` | string | Plain-text body. Omitted when the email has no text part. | | `html` | string | HTML body. Omitted when the email has no HTML part. | | `headers` | object | All parsed headers as a string map (`From`, `To`, `Return-Path`, `Received-Spf`, DKIM signatures, etc.). | | `spf` | string | SPF result: `pass`, `fail`, `softfail`, `neutral`, `none`, … | | `dkim` | string | DKIM result: `pass`, `fail`, `none`, … | | `dmarc` | string | DMARC result: `pass`, `fail`, `none`, … | | `attachments` | object | Attachments. Omitted when there are none, or when [attachments are dropped](https://webhookrelay.com/docs/email/payload/docs/email/filtering-and-policy/). | ### [Attachment object](#attachment-object) | Field | Type | Notes | | --- | --- | --- | | `name` | string | The attachment filename. | | `content_type` | string | MIME type, e.g. `application/pdf`, `image/png`. | | `size` | number | Size in bytes. | | `content` | string | The attachment bytes, **base64-encoded**, inlined in the JSON. Omitted when dropped or truncated. | | `truncated` | boolean | `true` when the content was stripped or the per-message attachment cap was exceeded — the metadata (`name`, `content_type`, `size`) is still present, but `content` is omitted. See [policy](https://webhookrelay.com/docs/email/payload/docs/email/filtering-and-policy/). | ## [Using the fields in a transform](#using-the-fields-in-a-transform) Attach a [function](https://webhookrelay.com/docs/email/payload/docs/webhooks/functions/) to reshape the email before it reaches your endpoint — for example to extract just what you need, or to build a chat message: ``` const email = JSON.parse(r.body); const slack = { text: \`:email: *${email.subject}* from ${email.from_name || email.from}\n${email.text}\` }; r.setBody(JSON.stringify(slack)); r.setHeader("Content-Type", "application/json"); ``` See the [webhook → Slack/Discord/Teams formatter](https://webhookrelay.com/docs/email/payload/webhook-message-formatter/) to build that message visually and copy the function. Did this page help you? --- --- title: GCP Cloud Storage | WebhookRelay meta: "og: title": "GCP Cloud Storage" description: Receive GCS object notifications as webhooks and upload webhook data to Google Cloud Storage using Webhook Relay service connections. url: https://webhookrelay.com/docs/service-connections/gcp_gcs.md file: /docs/service-connections/gcp_gcs.md --- ![Stripes](https://webhookrelay.com/docs/service-connections/gcp_gcs/images/stripes.svg) Documentation **Fundamentals** # **GCP Cloud Storage** Receive GCS object notifications as webhooks and upload webhook data to Google Cloud Storage using Webhook Relay service connections. Connect Webhook Relay to **Google Cloud Storage (GCS)** to store incoming webhook data as GCS objects (output). ## [Prerequisites](#prerequisites) - A [GCP service connection](https://webhookrelay.com/docs/service-connections/gcp_gcs/docs/service-connections/) with a service account that has Cloud Storage permissions - A GCS bucket in your GCP project ### [GCP Roles](#gcp-roles) **For GCS Output (upload objects):** - `roles/storage.objectCreator` ## [GCS Output — Upload Webhook Data to Cloud Storage](#gcs-output-upload-webhook-data-to-cloud-storage) GCS outputs store incoming webhook data as objects in your GCS bucket. Each webhook is saved as a separate file. ![GCP GCS output](https://webhookrelay.com/docs/service-connections/gcp_gcs/images/docs/sc/add_gcp_gcs.png) ### [Configuration](#configuration) | Field | Required | Description | | --- | :---: | --- | | `bucket_name` | Yes | GCS bucket name | | `prefix` | No | Object name prefix (e.g. `webhooks/`) | | `file_format` | No | Storage format: `json` (default), `body_only`, `har` | ### [Object Path](#object-path) Objects are stored with a date-based path: ``` {prefix}/{year}/{month}/{day}/{log_id}.json ``` For example: `webhooks/2026/02/24/.json` ![Browse your GCP GCS bucket](https://webhookrelay.com/docs/service-connections/gcp_gcs/images/docs/sc/sc_gcs_buckets.png) ## [Example: Bridge AWS SNS to GCS](#example-bridge-aws-sns-to-gcs) Archive [AWS SNS](https://webhookrelay.com/docs/service-connections/gcp_gcs/docs/service-connections/aws_sns/) notifications as objects in a GCS bucket. Useful when your storage and analytics are on GCP but events originate in AWS: 1. Create an [AWS service connection](https://webhookrelay.com/docs/service-connections/gcp_gcs/docs/service-connections/) with SNS subscribe permissions 2. Create a [GCP service connection](https://webhookrelay.com/docs/service-connections/gcp_gcs/docs/service-connections/) with GCS write permissions 3. Create a bucket in Webhook Relay 4. Add an **AWS SNS input** on the bucket 5. Add a **GCS output** on the bucket Every message published to the SNS topic is stored as an object in your GCS bucket. ### [Transform Before Storing](#transform-before-storing) Use a [Function](https://webhookrelay.com/docs/service-connections/gcp_gcs/docs/webhooks/functions/) to extract or reshape the data before writing to GCS: ``` const snsMessage = JSON.parse(r.body) // Store just the message content with metadata const archived = { source: "aws-sns", topic: snsMessage.TopicArn, message: JSON.parse(snsMessage.Message), timestamp: snsMessage.Timestamp } r.setBody(JSON.stringify(archived)) ``` See the [JSON encoding](https://webhookrelay.com/docs/service-connections/gcp_gcs/docs/webhooks/functions/manipulating-json/) guide for more transformation examples. Did this page help you? --- --- title: Sender filtering & policy | WebhookRelay meta: "og: title": "Sender filtering & policy" description: Harden an email input: restrict inbound mail to an allowlist of From addresses, cap or drop attachments, and rely on SPF/DKIM/DMARC and rate limiting. url: https://webhookrelay.com/docs/email/filtering-and-policy.md file: /docs/email/filtering-and-policy.md --- ![Stripes](https://webhookrelay.com/docs/email/filtering-and-policy/images/stripes.svg) Documentation **Fundamentals** # **Sender filtering & policy** Harden an email input: restrict inbound mail to an allowlist of From addresses, cap or drop attachments, and rely on SPF/DKIM/DMARC and rate limiting. An inbound email address can receive mail from anyone, so each email input has policy controls to keep it safe and quiet. ## [Restrict senders (From allowlist)](#restrict-senders-from-allowlist) By default an email input accepts mail from any sender. Add one or more **allowed From addresses** to restrict it: when the allowlist is non-empty, only those senders are accepted and everything else is **silently dropped** (the sender does not get a bounce, and nothing is relayed to your endpoint). ![Setting an allowed From-addresses filter on an email input](https://webhookrelay.com/docs/email/filtering-and-policy/images/docs/email/email_filter.png) - Matching is **exact** and **case-insensitive** on the message's `From` address (e.g. `alerts@stripe.com`). - Leave the allowlist empty to accept any sender. - This is a hardening control, not authentication — combine it with the SPF/DKIM/DMARC results below if you need to trust the sender's domain. ## [Enable / disable the address](#enable-disable-the-address) Each email input has an **enabled** switch. Disable it to stop accepting mail without deleting the input or losing the address; mail sent while disabled is rejected. Delete the input to free the address entirely. ## [Attachments](#attachments) Attachments are inlined into the JSON as base64 (`attachments[].content`). Two controls keep payloads manageable: - **Drop attachments** — skip attachment parsing and storage entirely. The `attachments` metadata may still be present with `truncated: true`, but no `content` is delivered. Use this when you only care about the email body. - **Attachment size cap** — a per-message total cap on attachment bytes. When exceeded, attachments are **truncated**: `name`, `content_type` and `size` are kept, `content` is omitted, and `truncated` is set to `true`. A server-wide default applies unless you set a lower per-input cap. See the [payload reference](https://webhookrelay.com/docs/email/filtering-and-policy/docs/email/payload/#attachment-object) for the attachment fields. ## [Authentication results](#authentication-results) Every parsed message includes the receiving mail server's **`spf`**, **`dkim`** and **`dmarc`** results (e.g. `pass`, `fail`, `none`). Check these in a [transform function](https://webhookrelay.com/docs/email/filtering-and-policy/docs/webhooks/functions/) or at your endpoint before trusting a message — for example, drop anything where `dmarc` isn't `pass` for a domain you expect. ``` const email = JSON.parse(r.body); if (email.dmarc !== "pass") { r.setResponseStatus(202); // accept but ignore return; } ``` ## [Rate limiting & abuse controls](#rate-limiting-abuse-controls) - **Per-input rate limit** — each address is rate limited to absorb bursts and abuse; excess mail is rejected (the sending server may retry). - **Message size cap** — oversized messages are rejected. - **Unknown / disabled addresses** — mail to an address that doesn't resolve is bounced; mail to a disabled input is rejected. ## [Related](#related) - [Receive emails as webhooks](https://webhookrelay.com/docs/email/filtering-and-policy/docs/email/) — set up an email input. - [Create & poll email addresses from the CLI](https://webhookrelay.com/docs/email/filtering-and-policy/docs/email/cli/) — set the sender allowlist with `--filter-from`. - [Email webhook payload](https://webhookrelay.com/docs/email/filtering-and-policy/docs/email/payload/) — every field. - [Transform functions](https://webhookrelay.com/docs/email/filtering-and-policy/docs/webhooks/functions/) — filter or reshape messages in flight. Did this page help you? --- --- title: Create & poll email addresses from the CLI | WebhookRelay meta: "og: title": "Create & poll email addresses from the CLI" description: Use the relay CLI to create inbound email addresses and read the messages they receive — one command to create, then poll the bucket for parsed JSON. url: https://webhookrelay.com/docs/email/cli.md file: /docs/email/cli.md --- ![Stripes](https://webhookrelay.com/docs/email/cli/images/stripes.svg) Documentation **Fundamentals** # **Create & poll email addresses from the CLI** Use the relay CLI to create inbound email addresses and read the messages they receive — one command to create, then poll the bucket for parsed JSON. The [`relay` CLI](https://webhookrelay.com/docs/email/cli/docs/installation/cli/) can create inbound email addresses and consume the messages they receive, without opening the dashboard. It's the quickest way to grab a throwaway inbox, wire an address into a script, or watch mail arrive in your terminal. Two commands do the work: - **`relay email create`** — create an inbound address (optionally on a fresh throwaway bucket). - **`relay events`** — pull the parsed emails from the bucket as they arrive. > You need the CLI installed and authenticated first — see [Install the CLI](https://webhookrelay.com/docs/email/cli/docs/installation/cli/). All commands accept a bucket by **name or ID**. ## [Create an address](#create-an-address) The fastest path — no arguments — creates a throwaway bucket and prints a ready-to-use address: ``` $ relay email create Inbound email address created. Address: eb9649a3-b781-4158-be69-bb210596e759@in.webhookrelay-mail.com Bucket: email-c8e44869 (9bf342ce-9582-42ea-8662-ac73d60e2db6, created for you) Input: eb9649a3-b781-4158-be69-bb210596e759 Poll it with: relay events --bucket email-c8e44869 --follow ``` The address is the input's ID at your environment's inbound domain (`@in.webhookrelay-mail.com`). Send mail to it and every message is parsed into [JSON](https://webhookrelay.com/docs/email/cli/docs/email/payload/) and stored on the bucket. Attach the address to an **existing bucket** with `--bucket`, and restrict who may send with `--filter-from` (repeatable; exact-match [sender allowlist](https://webhookrelay.com/docs/email/cli/docs/email/filtering-and-policy/)): ``` # Into an existing bucket, only accept mail from one sender relay email create --bucket signups --filter-from noreply@stripe.com # Several allowed senders, drop attachments, give it a name relay email create --bucket signups \ --name "stripe-receipts" \ --filter-from alerts@stripe.com \ --filter-from ci@stripe.com \ --no-attachments ``` For scripts, `--json` prints the address and IDs as JSON: ``` $ relay email create --bucket signups --name stripe-receipts --filter-from noreply@stripe.com --json { "email_address": "15a51b42-7de4-42ff-8628-4645565fa8b1@in.webhookrelay-mail.com", "input_id": "15a51b42-7de4-42ff-8628-4645565fa8b1", "bucket_id": "9bf342ce-9582-42ea-8662-ac73d60e2db6", "bucket_name": "signups", "allowed_senders": ["noreply@stripe.com"] } # Capture just the address $ ADDR=$(relay email create --json | jq -r .email_address) ``` **When you don't pass **`--bucket`**, **`relay email create`** provisions a throwaway bucket for the address. If the address can't be created afterwards, that throwaway bucket is cleaned up automatically — a bucket you pass with **`--bucket`** is never removed.** ### [`relay email create` flags](#relay-email-create-flags) | Flag | Description | | --- | --- | | `-b, --bucket ` | Bucket to attach the address to. A throwaway bucket is created if omitted. | | `--name ` | Friendly name for the email input. | | `--filter-from ` | Only accept mail from this sender (repeatable; exact, case-insensitive match). | | `--no-attachments` | Drop attachments instead of storing them. | | `--json` | Output the created address as JSON. | ## [List addresses in a bucket](#list-addresses-in-a-bucket) Lost an address? List a bucket's inbound addresses: ``` $ relay email list --bucket signups ADDRESS NAME FILTER STATUS 15a51b42-7de4-42ff-8628-4645565fa8b1@in.webhookrelay-mail.com stripe-receipts noreply@stripe.com - eb9649a3-b781-4158-be69-bb210596e759@in.webhookrelay-mail.com - any sender - ``` Add `--json` for the full objects (including `email_input` policy). ## [Poll for email](#poll-for-email) `relay events` consumes the bucket's **pull-delivery queue**: each call returns the emails that haven't been delivered yet and marks them delivered, so repeated calls drain the queue. Inbound emails show up with their sender and subject: ``` $ relay events --bucket signups ID AGE FROM SUBJECT 373e52cb-764e-4f0f-9c8f-6b1e2a0d9f11 6 seconds noreply@stripe.com Your receipt ``` Use `--follow` (like `tail -f`) to block and print new emails as they land: ``` relay events --bucket signups --follow ``` Pull a larger batch, look further back, or restrict to a single output: ``` # Up to 50 per poll, looking back 1 hour relay events --bucket signups --limit 50 --max-age 1h # Only events routed to a specific output relay events --bucket signups --output out_1a2b ``` ### [Scripting](#scripting) `--json` emits the full event objects; `--body` emits just the parsed-email JSON (one per line), which is handy for piping into `jq`: ``` # Extract sender + subject of each email relay events --bucket signups --json | jq '.[].body | fromjson | {from, subject}' # Stream raw parsed emails relay events --bucket signups --body # Only the event IDs (e.g. to feed into \`relay inspect\`) relay events --bucket signups -q ``` ### [`relay events` flags](#relay-events-flags) | Flag | Description | | --- | --- | | `-b, --bucket ` | Bucket to consume from (required). | | `--output ` | Only consume events for this output ID. | | `--limit ` | Max events per poll (max 100; CLI default 20). | | `--max-age ` | How far back to look for undelivered events (Go duration, e.g. `1h`; server default 24h). | | `-f, --follow` | Keep polling and print new events as they arrive. | | `--interval ` | Poll interval when following (default `2s`). | | `--json` | Output full event objects as JSON. | | `--body` | Print only the raw event bodies (parsed email JSON). | | `--format ` | Pretty-print events using a Go template. | | `-q, --quiet` | Only display event IDs. | ### [How polling works](#how-polling-works) `relay events` is a **consuming** queue: returning an email to you _is_ its delivery, so each row is marked delivered (`RECEIVED → SENT`) before the response is sent and won't appear on the next poll. This makes it a reliable "process each email once" channel: - The queue **drains** — keep polling (or use `--follow`) until you get an empty page. - If you consumed an email but failed to handle it, correct the outcome afterwards with `relay api` (`PUT /v1/logs/{id}`) so it can be retried. - For a **non-destructive** view of everything a bucket received, use `relay logs` and `relay inspect` instead — they read the audit log without consuming. ## [Related](#related) - [Receive emails as webhooks](https://webhookrelay.com/docs/email/cli/docs/email/) — the feature overview and dashboard flow. - [Email webhook payload](https://webhookrelay.com/docs/email/cli/docs/email/payload/) — every field your events contain. - [Sender filtering & policy](https://webhookrelay.com/docs/email/cli/docs/email/filtering-and-policy/) — allowlists, attachments, limits. - [Install the CLI](https://webhookrelay.com/docs/email/cli/docs/installation/cli/) — get the `relay` binary. Did this page help you? --- --- title: Account management | WebhookRelay meta: "og: title": "Account management" description: How to manage your account, change email address, password or delete your account url: https://webhookrelay.com/docs/account/account-management.md file: /docs/account/account-management.md --- ![Stripes](https://webhookrelay.com/docs/account/account-management/images/stripes.svg) Documentation **Fundamentals** # **Account management** How to manage your account, change email address, password or delete your account ## [Change email address](#change-email-address) To change your email address, please visit your [account details page](https://my.webhookrelay.com/account) and click on "danger zone" and then click "change email". Email changing is not applicable to sub-accounts or accounts that have registered using Google, GitHub or SSO (SAML with Active Directory, Okta or similar). ## [Change password](#change-password) To change your password, please visit your [account details page](https://my.webhookrelay.com/account) and click on "change password". Enter your current password and the new password. ## [Transfer account ownership](#transfer-account-ownership) If you wish to transfer your account to another user, please contact us at [](https://webhookrelay.com/docs/account/account-management//mailto:info@webhookrelay.com) [info@webhookrelay.com](https://webhookrelay.com/docs/account/account-management//mailto:info@webhookrelay.com) or using the chat interface within your dashboard. In the request please supply the new owner's email address and the reason for transfer. Did this page help you? --- --- title: Billing & subscriptions | WebhookRelay meta: "og: title": "Billing & subscriptions" description: How to manage your billing and subscriptions, updating payment methods and billing address, viewing invoices url: https://webhookrelay.com/docs/account/billing-and-subscriptions.md file: /docs/account/billing-and-subscriptions.md --- ![Stripes](https://webhookrelay.com/docs/account/billing-and-subscriptions/images/stripes.svg) Documentation **Fundamentals** # **Billing & subscriptions** How to manage your billing and subscriptions, updating payment methods and billing address, viewing invoices ## [Upgrade your plan](#upgrade-your-plan) To upgrade your plan, please visit [plans page](https://my.webhookrelay.com/account/plan) and select the plan you want to upgrade to and click "select". We do pro-rate subscriptions so you will be charged the difference between the new and old plan if it's in the middle of the month. ## [Adding new card](#adding-new-card) To add a new card, please visit [billing page](https://my.webhookrelay.com/account) and click on "add payment method". This will redirect you to Stripe's page where you can securely add your new card details. ### [Updating billing details](#updating-billing-details) You can also update your billing details such as name, address and tax ID (VAT number) here. Once updated, click "save" to apply the changes. These changes will only be visible in your future invoices. ## [Viewing invoices](#viewing-invoices) To view and download your past invoices you can access them through [account details page](https://my.webhookrelay.com/account) and then clicking "invoices" which will take you to the [invoices page](https://my.webhookrelay.com/account/invoices). ## [Downgrade your plan](#downgrade-your-plan) To downgrade your plan, please visit [plans page](https://my.webhookrelay.com/account/plan) and select the plan you want to downgrade to and click "select". We do pro-rate at the end of the month so you will be refunded the difference between the new and old plan at the end of the billing cycle. Did this page help you? --- --- title: Webhook Relay on ClawHub | WebhookRelay meta: "og: title": "Webhook Relay on ClawHub" description: ClawHub is an open registry for AI agent Skills. The Webhook Relay skill teaches agents to forward, tunnel, debug and schedule webhooks in one install. url: https://webhookrelay.com/docs/clawhub.md file: /docs/clawhub.md --- ![Stripes](https://webhookrelay.com/docs/clawhub/images/stripes.svg) Documentation **Fundamentals** # **Webhook Relay on ClawHub** ClawHub is an open registry for AI agent Skills. The Webhook Relay skill teaches agents to forward, tunnel, debug and schedule webhooks in one install. ## [What is ClawHub?](#what-is-clawhub) [ClawHub](https://clawhub.ai) is an open registry for **AI agent Skills** (and plugins) — the small instruction packs that teach Claude and other skill-aware agents how to perform a specific task. Think of it as a package registry, but for agent capabilities instead of code libraries. What it does, in short: - **Browse and search** skills published by the community, each with its rendered `SKILL.md`, file list, and version history. - **Install in one command** with the `openclaw` / `clawhub` CLI, which downloads the skill and extracts it into your agent's skills directory. - **Security audits** — every published skill is scanned (malware, telemetry, and agentic-risk checks) and shows a pass/review badge. - **Open and free** — all skills are published under the permissive `MIT-0` license; there are no paid skills or paywalls. ## [How Webhook Relay helps](#how-webhook-relay-helps) [Webhook Relay](https://webhookrelay.com) publishes a **Webhook Relay** skill on ClawHub that turns the platform into a capability your agent can drive directly. With it installed, an agent knows how to: - **Forward webhooks to a private/internal destination** — receive provider webhooks (Stripe, GitHub, Shopify, CI…) on a public URL and relay them to `localhost`, a LAN host, or a Kubernetes service with no public IP. - **Expose a local or internal service to the internet** — publish a stable public HTTPS/TCP tunnel without opening firewall ports. - **Debug webhooks** — capture and inspect exactly what a provider sends with a free, no-signup bin, and verify HMAC signatures. - **Forward, fan out, transform, and schedule** — relay server-side to public URLs, deliver one webhook to many destinations, reshape payloads with JavaScript functions, and fire recurring (cron) webhooks. It's the same toolkit documented across these docs, packaged as a single skill so an agent can set it up correctly from a single prompt. ## [Install](#install) The skill lives at **[clawhub.ai/rusenask/webhook-relay](https://clawhub.ai/rusenask/webhook-relay)**. Install it with the CLI: ``` openclaw skills install webhook-relay ``` Or open the listing and use the **Download** button / copy the install command. Once installed, your agent picks up the skill automatically and can drive the `relay` CLI and bin API. > The bin (debugging) workflow needs nothing but `curl`. Every other workflow uses the `relay` CLI — see [CLI installation](https://webhookrelay.com/docs/clawhub/docs/installation/cli/), then `relay login`. ## [Other ways to install the skills](#other-ways-to-install-the-skills) ClawHub is one distribution channel. The same skills are also available straight from the open-source repository and other registries — see [Agent Skills](https://webhookrelay.com/docs/clawhub/docs/skills/) for `npx skills add webhookrelay/skills`, the Claude Code plugin marketplace, and manual install. ## [Links](#links) - [Webhook Relay on ClawHub](https://clawhub.ai/rusenask/webhook-relay) - [ClawHub](https://clawhub.ai) — the open agent-skill registry - [Agent Skills](https://webhookrelay.com/docs/clawhub/docs/skills/) — all install channels and the skill list - [MCP server](https://webhookrelay.com/docs/clawhub/docs/mcp/) — give an agent live, typed tools against your account - [CLI installation](https://webhookrelay.com/docs/clawhub/docs/installation/cli/) Did this page help you? --- --- title: Teams and sub-accounts | WebhookRelay meta: "og: title": "Teams and sub-accounts" description: How to create teams and invite team members to your Webhook Relay account url: https://webhookrelay.com/docs/account/team.md file: /docs/account/team.md --- ![Stripes](https://webhookrelay.com/docs/account/team/images/stripes.svg) Documentation **Fundamentals** # **Teams and sub-accounts** How to create teams and invite team members to your Webhook Relay account **Webhook Relay supports teams so you can invite team members to your account. This feature is available in Business, Pro and Enterprise plans.** ## [Invite new team members](#invite-new-team-members) To invite a new team member, please visit your [account details page](https://my.webhookrelay.com/account) and click on "manage sub-accounts" and then click "create sub-account". Enter the email address of the new team member and click "create". You will be presented with temporary credentials. Once they login they will immediately have to update their password. Alternatively you can add them using their GitHub or Google emails. ## [Remove team members](#remove-team-members) To remove a team member, please visit your [sub-accounts management page](https://my.webhookrelay.com/account/sub), click on the member you want to remove and click "delete". Did this page help you? --- --- title: Demoing your website | WebhookRelay meta: "og: title": "Demoing your website" description: How to expose your local web server to the internet without public IP or router changes url: https://webhookrelay.com/docs/tunnels/demoing-your-website.md file: /docs/tunnels/demoing-your-website.md --- ![Stripes](https://webhookrelay.com/docs/tunnels/demoing-your-website/images/stripes.svg) Documentation **Fundamentals** # **Demoing your website** How to expose your local web server to the internet without public IP or router changes Once in a while there is a need to share your work-in-progress website, built on technologies like [NextJS](https://nextjs.org/), [Nuxt](https://nuxt.com/), [hugo](https://gohugo.io/) or any other framework, tool, etc.. Sometimes it is way too early in the development cycle to set up automated (or not) deployment just to get some feedback; this is where tunnels come in. In this example we will run a local NextJS server. I will create an example app with: ``` npx create-next-app@latest \ nextjs-blog \ --use-npm \ --example "https://github.com/vercel/next-learn/tree/main/basics/learn-starter" ``` and start it ``` cd nextjs-blog npm run dev ``` I can view it on `http://localhost:3000`: ![nextjs starter server](https://webhookrelay.com/docs/tunnels/demoing-your-website/images/docs/tunnels/nextjs.png) This is running on my local machine, and I want to share it with my friends. Now, to expose it to the internet, I can use the following command: ``` relay connect --name my-tunnel http://localhost:3000 ``` These commands have to be run in two separate terminal windows: ![running commands](https://webhookrelay.com/docs/tunnels/demoing-your-website/images/docs/tunnels/commands.png) Click on the link in the terminal to open the browser and view the website: ![nextjs starter server](https://webhookrelay.com/docs/tunnels/demoing-your-website/images/docs/tunnels/nextjs-tunnel.png) That's it! Did this page help you? --- --- title: Regions | WebhookRelay meta: "og: title": Regions description: Regional tunnel servers are available in a number of different locations to enable fast & low latency traffic to your applications. url: https://webhookrelay.com/docs/tunnels/regions.md file: /docs/tunnels/regions.md --- ![Stripes](https://webhookrelay.com/docs/tunnels/regions/images/stripes.svg) Documentation **Fundamentals** # **Regions** Regional tunnel servers are available in a number of different locations to enable fast & low latency traffic to your applications. Regional tunnel servers are available in a number of different locations to enable fast & low latency traffic to your applications. Tunnels in different regions will get different domains/subdomains. Webhook Relay regions: - default region is in Belgium (country within Europe can change without notice) - au - Sydney, Australia - us-west - Silicon Valley, US ### [Usage](#usage) **relay CLI** When using **relay** CLI, specify --region flag: ``` relay connect -s whr-demo --region us-west :4000 Connecting: http://whr-demo.us-west.webrelay.io <----> http://127.0.0.1:4000 ``` **webhookrelayd container** When using **webhookrelayd** there are two options to specify region: - First one is to set environment variable REGION=us-west: ``` docker run --network host -e REGION=us-west -e KEY= -e SECRET= webhookrelay/webhookrelayd:latest --mode tunnel -t jupyter ``` - Second option is to set it via the command flag: ``` docker run --network host -e KEY= -e SECRET= webhookrelay/webhookrelayd:latest --mode tunnel -t jupyter --region us-west ``` Did this page help you? --- --- title: n8n Email Trigger (Inbound Email) | WebhookRelay meta: "og: title": "n8n Email Trigger (Inbound Email)" description: Trigger n8n workflows from inbound email — no IMAP, mail server or polling. Get a unique address, parsed to JSON and streamed to self-hosted n8n. url: https://webhookrelay.com/docs/tutorials/n8n/email-trigger.md file: /docs/tutorials/n8n/email-trigger.md --- ![Stripes](https://webhookrelay.com/docs/tutorials/n8n/email-trigger/images/stripes.svg) Documentation **Fundamentals** # **n8n Email Trigger (Inbound Email)** Trigger n8n workflows from inbound email — no IMAP, mail server or polling. Get a unique address, parsed to JSON and streamed to self-hosted n8n. The usual way to receive email in n8n is the **IMAP** node, which polls a mailbox you have to own and configure. The [Webhook Relay Email Trigger](https://www.npmjs.com/package/n8n-nodes-webhookrelay) skips all of that: it gives you a **unique inbound address**, parses every message to JSON, and streams it to n8n over an **outbound WebSocket** — no IMAP, no mail server, no polling, and n8n is never exposed. If you haven't installed the node yet, follow steps 1–2 of the [Webhook Trigger tutorial](https://webhookrelay.com/docs/tutorials/n8n/email-trigger/docs/tutorials/n8n/webhook-trigger/) (install `n8n-nodes-webhookrelay` and add the API-key credential) — the same credential works for both triggers. ## [1. Add the Email Trigger](#_1-add-the-email-trigger) Add a **Webhook Relay Email Trigger**, give it a **Bucket** name (e.g. `n8n-email`), and optionally restrict **Allowed Senders**. Open the **Email Address** field to get your unique inbound address (`@in.webhookrelay-mail.com`). ![Webhook Relay Email Trigger parameters](https://webhookrelay.com/docs/tutorials/n8n/email-trigger/images/tutorials/n8n/04-email-config.png) ## [2. Send a test email](#_2-send-a-test-email) **Activate** the workflow, then send an email to that address from any client. ![Sending a test email to the Webhook Relay inbound address](https://webhookrelay.com/docs/tutorials/n8n/email-trigger/images/tutorials/n8n/send-test-email.png) ## [3. Watch it arrive](#_3-watch-it-arrive) Within a second or two the parsed message reaches n8n. Each email arrives as structured JSON — `from`, `subject`, `text`, `html`, `attachments`, `spf`/`dkim`/`dmarc` and the full `headers` map — ready for downstream nodes. You can inspect exactly what was received in the [dashboard](https://my.webhookrelay.com/buckets). ![Parsed inbound email in the Webhook Relay dashboard](https://webhookrelay.com/docs/tutorials/n8n/email-trigger/images/tutorials/n8n/webhookrelay-email-debug.png) ``` { "from": "jon@example.com", "from_name": "Jon Snow", "recipient": "@in.webhookrelay-mail.com", "subject": "Order #4821 confirmed", "text": "plain body", "html": "

html body

", "spf": "pass", "dkim": "pass", "dmarc": "pass" } ``` See the full [payload reference](https://webhookrelay.com/docs/tutorials/n8n/email-trigger/docs/email/payload/) for every field. ## [Get started](#get-started) [Create a free Webhook Relay account](https://my.webhookrelay.com/register), add the Email Trigger, and turn support requests, alerts and form submissions into n8n workflows — no IMAP or mailbox required. Receiving **HTTP webhooks** too? See the [n8n Webhook Trigger](https://webhookrelay.com/docs/tutorials/n8n/email-trigger/docs/tutorials/n8n/webhook-trigger/). Did this page help you? --- --- title: Team Member Roles | WebhookRelay meta: "og: title": "Team Member Roles" description: Control what each collaborator can see and do in your Webhook Relay organization with Admin, Billing, Member and Viewer roles. url: https://webhookrelay.com/features/team-member-roles.md file: /features/team-member-roles.md --- ![Stripes](https://webhookrelay.com/features/team-member-roles/images/stripes.svg) **Plans: Business, Pro and Enterprise** FEATURES # **Team Member Roles** Control what each collaborator can see and do in your Webhook Relay organization with Admin, Billing, Member and Viewer roles. ![Invite a member dialog with role options: Admin, Billing, Member and Viewer](https://webhookrelay.com/features/team-member-roles/images/features/custom-roles/invite-member.png) Team member roles let you grant the right level of access to each person in your organization. Convert your account into an organization, invite colleagues with their own Webhook Relay login, and assign a role that controls resources, billing and membership. For the full overview, see [custom roles](https://webhookrelay.com/features/team-member-roles/features/custom-roles/). ## [Roles](#roles) - **Admin** — full access: members, billing, resources - **Billing** — resources + billing management - **Member** — resources only; no billing or member management - **Viewer** — read-only: sees everything, changes nothing, no billing ## [Why roles matter](#why-roles-matter) - **Least privilege** — give each person exactly the access they need - **Safer onboarding** — invite with a scoped role; revoke when someone leaves - **Billing control** — only Admin and Billing can touch the plan - **Accountability** — pair with [audit logs](https://webhookrelay.com/features/team-member-roles/features/audit-logs/) so every action is attributable Combine with [Single Sign-On (SSO)](https://webhookrelay.com/features/team-member-roles/features/sso/) and [teams](https://webhookrelay.com/features/team-member-roles/features/teams/) for organization-scale webhook management. [Create an account](https://my.webhookrelay.com/register) or [contact sales](https://webhookrelay.com/features/team-member-roles/contact-sales/) to discuss team plans. ![Stripes](https://webhookrelay.com/features/team-member-roles/images/stripes-dark.svg) ## **Start forwarding webhooks in minutes ** Connect a source, pick a destination, and Webhook Relay handles delivery, retries and transforms. Set up your first webhook in under five minutes. [Start for free ->](https://my.webhookrelay.com/register) Free plan · No credit card required · 7-day money-back guarantee on paid plans --- --- title: n8n WhatsApp Cloud API Webhook Setup | WebhookRelay meta: "og: title": "n8n WhatsApp Cloud API Webhook Setup" description: Receive WhatsApp Cloud API webhooks in self-hosted n8n: a stable public URL for Meta, the verify-token handshake answered automatically, no public IP. url: https://webhookrelay.com/docs/tutorials/n8n/whatsapp-cloud-api-webhook.md file: /docs/tutorials/n8n/whatsapp-cloud-api-webhook.md --- ![Stripes](https://webhookrelay.com/docs/tutorials/n8n/whatsapp-cloud-api-webhook/images/stripes.svg) Documentation **Fundamentals** # **n8n WhatsApp Cloud API Webhook Setup** Receive WhatsApp Cloud API webhooks in self-hosted n8n: a stable public URL for Meta, the verify-token handshake answered automatically, no public IP. Meta's WhatsApp Cloud API only delivers webhooks to a **public HTTPS URL**, and before it sends anything it runs a **verification handshake** — a GET request your endpoint must answer by echoing the `hub.challenge` query value. That is two problems for self-hosted n8n: your instance usually is not public, and tunnels with rotating URLs force you to re-verify in the Meta dashboard every time the URL changes. This tutorial solves both with a stable Webhook Relay endpoint: Meta verifies **one permanent URL** (the handshake is answered by a tiny Function, automatically), and your n8n instance receives every message over an **outbound WebSocket** — no public IP, no open ports, nothing to re-verify. ``` Meta ──GET hub.challenge──▶ Webhook Relay (Function answers handshake) Meta ──POST messages──────▶ Webhook Relay (stable public URL) │ n8n ──outbound WebSocket─┘ ← n8n connects out; nothing inbound ``` ## [1. Install the Webhook Relay node in n8n](#_1-install-the-webhook-relay-node-in-n8n) In n8n go to **Settings → Community Nodes → Install** and enter `n8n-nodes-webhookrelay`. Then create an **API key** at [my.webhookrelay.com/tokens](https://my.webhookrelay.com/tokens) and add it as a **Webhook Relay API** credential. (The general setup, with screenshots, is in the [n8n Webhook Trigger tutorial](https://webhookrelay.com/docs/tutorials/n8n/whatsapp-cloud-api-webhook/docs/tutorials/n8n/webhook-trigger/).) ## [2. Create a bucket for WhatsApp](#_2-create-a-bucket-for-whatsapp) In [my.webhookrelay.com](https://my.webhookrelay.com/buckets) create a bucket named `whatsapp`. Its default input URL — `https://xxx.hooks.webhookrelay.com/...` — is permanent. This is the URL Meta will verify once and deliver to forever. ## [3. Answer the verify-token handshake with a Function](#_3-answer-the-verify-token-handshake-with-a-function) Meta's verification GET must be answered with the raw `hub.challenge` value. Attach this Function to the bucket's **input** so the handshake is handled at the edge (n8n never needs to see it): ``` // Answer Meta's one-time subscription verification; let real events through. var VERIFY_TOKEN = "my-verify-token"; // must match what you enter in Meta if (r.RequestMethod() === "GET") { if (r.RequestQuery("hub.verify_token") === VERIFY_TOKEN) { r.SetResponseStatusCode(200); r.SetResponseBody(r.RequestQuery("hub.challenge")); } else { r.SetResponseStatusCode(403); r.SetResponseBody("verification failed"); } } ``` POST deliveries (the actual messages) pass through untouched and get the bucket's normal `200` response, which is all Meta needs. ## [4. Point Meta at the bucket](#_4-point-meta-at-the-bucket) In the [Meta App Dashboard](https://developers.facebook.com/apps/): **WhatsApp → Configuration → Webhook → Edit**. Enter the bucket's input URL as the **Callback URL** and the same verify token as in the Function, then save — verification succeeds instantly. Click **Manage** and subscribe to the **`messages`** webhook field. ## [5. Receive messages in n8n](#_5-receive-messages-in-n8n) Add the **Webhook Relay Trigger** node to a workflow, select the `whatsapp` bucket, and activate. The node opens an outbound WebSocket and every WhatsApp delivery arrives as a workflow execution. Messages sit a few levels deep in Meta's envelope. A **Set** or **Code** node right after the trigger tidies that up: ``` // Code node: flatten the Cloud API envelope into one item per message const out = []; for (const entry of $json.body.entry || []) { for (const change of entry.changes || []) { for (const msg of change.value?.messages || []) { out.push({ json: { from: msg.from, type: msg.type, text: msg.text?.body, timestamp: msg.timestamp, contact: change.value.contacts?.[0]?.profile?.name, }}); } } } return out; ``` Status updates (`sent`/`delivered`/`read`) arrive on the same bucket with a `statuses` array instead of `messages` — filter on what is present. ## [Notes](#notes) - **Restarts are free.** Meta verified the bucket URL, not your n8n instance — redeploy or move n8n anytime, nothing to re-verify. - **Verify signatures if you need authenticity**: Meta signs POSTs with `X-Hub-Signature-256` (HMAC-SHA256 of the raw body with your app secret). The header reaches your workflow intact, so you can check it in a Code node — see [Verify a webhook signature](https://webhookrelay.com/docs/tutorials/n8n/whatsapp-cloud-api-webhook/blog/verify-webhook-signature/). - **Payload reference**: the full setup-and-payload story (including self-hosted gateways like WAHA) is in [WhatsApp Cloud API webhooks: setup, verify token and payloads](https://webhookrelay.com/docs/tutorials/n8n/whatsapp-cloud-api-webhook/blog/whatsapp-cloud-api-webhooks/). Did this page help you? --- --- title: Jenkins Multibranch Pipelines | WebhookRelay meta: "og: title": "Jenkins Multibranch Pipelines" description: Trigger Jenkins Multibranch Pipeline builds from GitHub webhooks without exposing Jenkins — branch and pull request jobs via the Webhook Relay plugin. url: https://webhookrelay.com/docs/tutorials/cicd/jenkins-plugin-multibranch.md file: /docs/tutorials/cicd/jenkins-plugin-multibranch.md --- ![Stripes](https://webhookrelay.com/docs/tutorials/cicd/jenkins-plugin-multibranch/images/stripes.svg) Documentation **Fundamentals** # **Jenkins Multibranch Pipelines** Trigger Jenkins Multibranch Pipeline builds from GitHub webhooks without exposing Jenkins — branch and pull request jobs via the Webhook Relay plugin. **Multibranch Pipeline** projects work with the [Webhook Relay Jenkins plugin](https://webhookrelay.com/docs/tutorials/cicd/jenkins-plugin-multibranch/docs/tutorials/cicd/jenkins-plugin/) just like regular jobs — pushes and pull requests on GitHub trigger builds on a Jenkins that has **no public IP**. But they are configured differently from regular jobs, and that trips people up: > **A Multibranch Pipeline has no _Build Triggers_ section.** Don't look for one — nothing is missing from your Jenkins. Branch and pull request jobs are created and triggered by the **branch source** reacting to the webhook events the plugin delivers, with no per-job trigger configuration at all. This page shows the full setup for GitHub. When it's done, every push and pull request travels GitHub → your Webhook Relay bucket → the plugin → `/github-webhook/`, and: - **Push to a branch** — the matching branch job builds. - **Push a new branch** — a job for it is created and built automatically. - **Open a pull request** — a `PR-` job is created and built. ## [Requirements](#requirements) - The Webhook Relay plugin installed, connected and with a webhook URL resolved — steps 1–3 of the [Jenkins Plugin tutorial](https://webhookrelay.com/docs/tutorials/cicd/jenkins-plugin-multibranch/docs/tutorials/cicd/jenkins-plugin/) (5 minutes). - The [GitHub Branch Source](https://plugins.jenkins.io/github-branch-source/) plugin (**Manage Jenkins → Plugins → Available**, search for _GitHub Branch Source_). It listens on the same `/github-webhook/` endpoint the Webhook Relay plugin already delivers to, so the Webhook Relay configuration stays exactly the same. - A repository with a `Jenkinsfile` on the branches you want built. The screenshots below use [webhookrelay/jenkins-multibranch-demo](https://github.com/webhookrelay/jenkins-multibranch-demo), a minimal public repo with a `Jenkinsfile` on every branch. ## [1. Create the Multibranch Pipeline](#_1-create-the-multibranch-pipeline) From **New Item**, enter a name and pick **Multibranch Pipeline**: ![New Item — Multibranch Pipeline](https://webhookrelay.com/docs/tutorials/cicd/jenkins-plugin-multibranch/images/tutorials/jenkins/plugin-06-multibranch-new-item.png) Under **Branch Sources**, add a **GitHub** source and set the **Repository HTTPS URL** to your repository (add a credential if it is private): ![Branch source — GitHub repository](https://webhookrelay.com/docs/tutorials/cicd/jenkins-plugin-multibranch/images/tutorials/jenkins/plugin-07-multibranch-branch-source.png) Note the `Jenkinsfile` needs **no** `triggers { githubPush() }` block — that trigger is only for regular Pipeline jobs. Save, and Jenkins scans the repository once, creating a job for every branch that has a `Jenkinsfile`. ## [2. Add the webhook events](#_2-add-the-webhook-events) In your repository's _Settings → Webhooks_, add the **Get Webhook URL** value from the plugin configuration as the Payload URL (content type `application/json`). One thing to watch: GitHub's default is to send _just the push event_. That covers branch builds — but if you also want pull request jobs, choose _Let me select individual events_ and tick **Pushes** and **Pull requests**. ## [3. Push, branch, open PRs](#_3-push-branch-open-prs) That's it. Push a commit and the matching branch job builds; push a new branch and its job appears and builds on its own: ![Multibranch Pipeline — branch jobs](https://webhookrelay.com/docs/tutorials/cicd/jenkins-plugin-multibranch/images/tutorials/jenkins/plugin-08-multibranch-branches.png) Pull requests get their own tab: ![Multibranch Pipeline — pull request jobs](https://webhookrelay.com/docs/tutorials/cicd/jenkins-plugin-multibranch/images/tutorials/jenkins/plugin-09-multibranch-prs.png) The build page shows the webhook as the cause — _Push event to branch main_: ![Branch build caused by a push event through Webhook Relay](https://webhookrelay.com/docs/tutorials/cicd/jenkins-plugin-multibranch/images/tutorials/jenkins/plugin-10-multibranch-push-event.png) Every delivery is also recorded on the bucket's [logs page](https://my.webhookrelay.com/buckets) together with the response Jenkins returned, so a webhook that didn't trigger a build is easy to trace. ## [GitLab and Bitbucket](#gitlab-and-bitbucket) The same idea applies to the other providers, with one twist: the [GitLab Branch Source](https://plugins.jenkins.io/gitlab-branch-source/) and [Bitbucket Branch Source](https://plugins.jenkins.io/cloudbees-bitbucket-branch-source/) plugins listen on their own endpoints (`/gitlab-webhook/post` and `/bitbucket-scmsource-hook/notify`) rather than on the plugin's preset paths. To deliver there, add an **internal output** to your bucket with the full destination URL (for example `http://localhost:8080/gitlab-webhook/post`) — when a bucket has an internal output, the plugin honours its path instead of the preset. ## [Troubleshooting](#troubleshooting) | Symptom | Fix | | --- | --- | | Looking for a _Build Triggers_ section | There isn't one on multibranch projects — the branch source handles triggering. | | Pushes build, pull requests don't | The GitHub webhook is only sending push events — tick **Pull requests** under _Let me select individual events_. | | Webhook arrives (bucket log shows `200`) but nothing builds | The job's branch source must be **GitHub** (not plain _Git_), and the repository URL must match the pushed repo. | | Nothing in the bucket logs | The repository webhook isn't pointing at the bucket's public URL — re-check the Payload URL against **Get Webhook URL**. | ## [Try it with Docker](#try-it-with-docker) The [plugin repository](https://github.com/jenkinsci/webhook-relay-plugin) ships a ready-to-run demo Jenkins (see the [Jenkins Plugin tutorial](https://webhookrelay.com/docs/tutorials/cicd/jenkins-plugin-multibranch/docs/tutorials/cicd/jenkins-plugin/)); install the _GitHub Branch Source_ plugin in it and follow the steps above against a fork of [jenkins-multibranch-demo](https://github.com/webhookrelay/jenkins-multibranch-demo). Did this page help you? --- --- title: Execute scripts on webhook | WebhookRelay meta: "og: title": "Execute scripts on webhook" description: Execute commands such as bash, python or ruby when webhooks are received url: https://webhookrelay.com/docs/tutorials/cicd/webhook-exec.md file: /docs/tutorials/cicd/webhook-exec.md --- ![Stripes](https://webhookrelay.com/docs/tutorials/cicd/webhook-exec/images/stripes.svg) Documentation **Fundamentals** # **Execute scripts on webhook** Execute commands such as bash, python or ruby when webhooks are received ## [Executing commands on a host machine](#executing-commands-on-a-host-machine) Relay agent lets executing any commands or scripts on a host machine when webhooks are received. To use this functionality, use `--relayer exec` option together with `--command python` (or any other command such as `node`) followed by optional arguments. For example to execute python script in a file called `my-script.py`: ``` relay forward --bucket my-bucket-name --relayer exec --command python my-script.py ``` Here: - **--bucket** specifies which bucket to subscribe to - **--relayer** specifies `exec` relayer (by default it would forward to an HTTP endpoint) - **--command** is the main command to execute, for bash scripts you would use `bash` command - **my-script.py** is the optional script/application file This script will receive webhook data through standard input ([stdin](https://en.wikipedia.org/wiki/Standard_streams)). You can read this data and optionally return a response. Response will be visible in the webhook log through the Webhook Relay web interface. > Executed script will always be marked with status code 200 even though it will not be forwarded to a destination by the relay agent. When running in a `--relayer exec` mode, agent will only execute commands without forwarding webhooks. To also forward webhooks, launch a second agent. ### [Python example](#python-example) Here is a simple Python script that will read from standard input and save the webhook payload into a file with a timestamp. ``` import sys from time import gmtime, strftime import datetime payload = sys.stdin.read() now = datetime.datetime.now() timestamp = str(now.strftime("%Y%m%d_%H:%M:%S")) filename = "webhook-" + timestamp + ".json" file = open(filename,"w") file.write(payload) file.close() print("received") ``` ### [Default input mode](#default-input-mode) By default **relay** agent only sends request body through the stdin into the script: ``` relay forward --bucket forward-a --relayer exec --command python my-script.py Executing command on webhooks: https://my.webhookrelay.com/v1/webhooks/1eff80d0-b258-4175-983c-b03f8961608d -> python my-script.py [i] Please ensure that your buckets have at least one internal output destination, otherwise this agent will not receive webhooks. ``` Now, send a webhook: ``` curl --request POST \ --url https://my.webhookrelay.com/v1/webhooks/1eff80d0-b258-4175-983c-b03f8961608d \ --data '{ "msg": "webhook msg" }' ``` File contents will be: ``` { "msg": "webhook msg" } ``` ### [JSON input mode](#json-input-mode) Launching **relay** agent you should see that it's now executing commands instead of forwarding webhooks: ``` relay forward --bucket forward-a --relayer exec --input-mode json --command python my-script.py Executing command on webhooks: https://my.webhookrelay.com/v1/webhooks/1eff80d0-b258-4175-983c-b03f8961608d -> python my-script.py [i] Please ensure that your buckets have at least one internal output destination, otherwise this agent will not receive webhooks. ``` Let's send another webhook: ``` curl --request POST \ --url https://my.webhookrelay.com/v1/webhooks/1eff80d0-b258-4175-983c-b03f8961608d \ --data '{ "msg": "webhook msg" }' ``` You should now see a file created with contents: ``` { "type": "webhook", "meta": { "bucked_id": "e1ea4726-89e1-4983-84ac-fc92cb153647", "bucket_name": "forward-a", "input_id": "1eff80d0-b258-4175-983c-b03f8961608d", "input_name": "Default public endpoint", "output_name": "https://bin.webhookrelay.com/#/bins/f8235d2a-2156-4b3e-a110-0cfd9ab3a3fa", "output_destination": "https://bin.webhookrelay.com/v1/webhooks/84575fa2-4d9d-4aa4-942f-4e5ffea52dae" }, "headers": { "Accept": [ "*/*" ], "Content-Length": [ "25" ], "User-Agent": [ "insomnia/6.6.2" ], }, "query": "", "body": "{\n\t\"msg\": \"webhook msg\"\n}", "method": "POST", "status": "", "message": "" } ``` ## [Background service configuration](#background-service-configuration) To execute commands when running in a [background service mode](https://docs.webhookrelay.com/installation-options/installation-options/background-service) add additional 'relayer' section: ``` version: "v1" key: your-secret-key # will be encrypted on startup secret: your-secret # will be encrypted on startup buckets: - bucket-1-name - bucket-2-name relayer: type: exec # inputMode: json # uncomment to receive full JSON payload with headers, query and other metadata timeout: 2 # timeout in seconds command: python commandArgs: [ 'my-script.py' ] ``` ## [Debugging scripts and commands](#debugging-scripts-and-commands) To debug your commands you can use standard shell pipes. For example to try the previous Python example without Webhook Relay: ``` echo my-data-here | python my-script.py ``` Or, if your test data is in a file: ``` cat test_data.json | python my-script.py ``` Did this page help you? --- --- title: n8n Webhook Trigger (No Public IP) | WebhookRelay meta: "og: title": "n8n Webhook Trigger (No Public IP)" description: Receive webhooks in self-hosted n8n with no public IP, port forwarding or tunnel. n8n connects out over a WebSocket and is never exposed to the internet. url: https://webhookrelay.com/docs/tutorials/n8n/webhook-trigger.md file: /docs/tutorials/n8n/webhook-trigger.md --- ![Stripes](https://webhookrelay.com/docs/tutorials/n8n/webhook-trigger/images/stripes.svg) Documentation **Fundamentals** # **n8n Webhook Trigger (No Public IP)** Receive webhooks in self-hosted n8n with no public IP, port forwarding or tunnel. n8n connects out over a WebSocket and is never exposed to the internet. To receive a webhook, n8n's built-in Webhook node needs a **public URL** — which means exposing your instance to the internet with a reverse proxy, a tunnel, or `--tunnel`. If your n8n runs **on-prem, behind a firewall, or on `localhost`**, that's the hard part. The [Webhook Relay Trigger](https://www.npmjs.com/package/n8n-nodes-webhookrelay) removes it. Webhook Relay gives your provider a stable public URL, verifies and answers the sender, and your n8n node **opens an outbound WebSocket** to receive each event. Nothing listens for inbound connections — no public IP, no tunnel, no agent. ``` Provider ──HTTP──▶ Webhook Relay (public URL, auth + response) │ n8n ──outbound WebSocket──┘ ← n8n connects out; nothing inbound ``` ## [1. Install the node](#_1-install-the-node) In n8n go to **Settings → Community Nodes → Install** and enter `n8n-nodes-webhookrelay`. Two triggers appear in the node picker. ![Webhook Relay Trigger and Email Trigger in the n8n node picker](https://webhookrelay.com/docs/tutorials/n8n/webhook-trigger/images/tutorials/n8n/01-node-list.png) ## [2. Add the credential](#_2-add-the-credential) Create an **API key** at [my.webhookrelay.com/tokens](https://my.webhookrelay.com/tokens), then in the node click **Set up credential** and paste it into a **Webhook Relay API** credential. ![Webhook Relay API credential in n8n](https://webhookrelay.com/docs/tutorials/n8n/webhook-trigger/images/tutorials/n8n/05-credential.png) ## [3. Configure the trigger](#_3-configure-the-trigger) Give it a **Bucket** name (e.g. `n8n`) — created automatically. Optionally set **Endpoint Authentication** (Basic or token) and the **Response** returned to the sender. Open the **Public URL** field to get the URL to hand your provider (`https://.hooks.webhookrelay.com`). ![Webhook Relay Trigger parameters](https://webhookrelay.com/docs/tutorials/n8n/webhook-trigger/images/tutorials/n8n/02-trigger-config.png) ## [4. Activate and test](#_4-activate-and-test) **Activate** the workflow, then send webhooks to that public URL. Requests appear in the [Webhook Relay dashboard](https://my.webhookrelay.com/buckets) and flow into n8n over the socket; the body, headers, query and method are all available to downstream nodes. ![Webhooks delivered to the n8n bucket](https://webhookrelay.com/docs/tutorials/n8n/webhook-trigger/images/tutorials/n8n/view-events.png) > **Test vs activate:** _Test this trigger_ captures a **single** event so you can build the workflow, then stops — that's expected. **Activate** the workflow to receive events continuously. The connection keeps itself alive with a ping every 15 s and reconnects immediately if it ever drops. **Throttle or rate-limit delivery:** the node creates an internal output on the bucket; open it in the dashboard to pace delivery (throttling) or tune per-output options without touching the node. ## [Get started](#get-started) [Create a free Webhook Relay account](https://my.webhookrelay.com/register), install the node, and receive webhooks in on-prem n8n in a few minutes — no public IP required. Receiving **email** instead? See the [n8n Email Trigger](https://webhookrelay.com/docs/tutorials/n8n/webhook-trigger/docs/tutorials/n8n/email-trigger/). Did this page help you? --- --- title: Webhook Relay - Features | WebhookRelay meta: "og: title": "Webhook Relay - Features" description: Explore the powerful features of Webhook Relay designed to streamline your webhook management and integrations. url: https://webhookrelay.com/features.md file: /features.md --- # **Features ** Explore the powerful features of Webhook Relay designed to streamline your webhook management and integrations. [![](https://webhookrelay.com/features/images/features/alerts/create-alert.png)

**Alerts**

Define alert policies on your buckets, choose where to get notified (Email, Slack, Discord, Teams, Telegram, Pushover, SMTP or webhook), and Webhook Relay opens and resolves incidents when delivery fails.](https://webhookrelay.com/features/features/alerts/) [![](https://webhookrelay.com/features/images/features/audit-logs/activity.png)

**Audit Logs**

See every action in your Webhook Relay account — who created, updated or deleted resources, when, and from which IP. Full activity tracking for organizations and teams.](https://webhookrelay.com/features/features/audit-logs/) [![](https://webhookrelay.com/features/images/features/custom-domains/cover.png)

**Custom Domains**

Receive webhooks on your own custom domain with Webhook Relay. Brand your endpoints, keep URLs stable and route traffic through domains you control.](https://webhookrelay.com/features/features/custom-domains/) [![](https://webhookrelay.com/features/images/features/custom-roles/invite-member.png)

**Custom Roles**

Convert your Webhook Relay account into an organization, invite other accounts as members, and assign Admin, Billing, Member or Viewer roles to control access and billing.](https://webhookrelay.com/features/features/custom-roles/) [![](https://webhookrelay.com/features/images/features/custom-subdomains/cover.png)

**Custom Subdomains**

Use a custom webhookrelay.com subdomain for your webhook endpoints — get memorable, stable URLs for receiving and forwarding webhooks to any destination.](https://webhookrelay.com/features/features/custom-subdomains/) [![](https://webhookrelay.com/features/images/features/durable-retries/cover.png)

**Durable Webhook Retries**

Never lose a webhook to a flaky endpoint. Webhook Relay retries failed deliveries for up to 30 days with exponential backoff, surviving outages.](https://webhookrelay.com/features/features/durable-retries/) [![](https://webhookrelay.com/features/images/features/forwarding-rules/cover.png)

**Forwarding Rules - Filter and Route Webhooks**

Filter and route webhooks on request body, JSON paths, query parameters, URL path and IP address. Control which webhooks reach each destination.](https://webhookrelay.com/features/features/forwarding-rules/) [![](https://webhookrelay.com/features/images/features/rewrite-host-header/cover.png)

**Rewriting Host Header**

Rewrite the Host header on forwarded webhooks and tunnels with Webhook Relay so you can expose local servers and virtual hosts to the internet correctly.](https://webhookrelay.com/features/features/rewrite-host-header/) [![](https://webhookrelay.com/features/images/features/sso/cover.png)

**Single Sign-On (SSO)**

Enable SAML-based Single Sign-On (SSO) for your Webhook Relay account with Okta, Azure AD and more — centralize authentication and simplify user management.](https://webhookrelay.com/features/features/sso/) [![](https://webhookrelay.com/features/images/features/static-ip/cover.png)

**Static Outgoing IP Address**

How to use a static outgoing IP address for your webhooks to unlock integrations that require whitelisting your IP address](https://webhookrelay.com/features/features/static-outgoing-ip/) [![](https://webhookrelay.com/features/images/features/custom-roles/invite-member.png)

**Team Member Roles**

Control what each collaborator can see and do in your Webhook Relay organization with Admin, Billing, Member and Viewer roles.](https://webhookrelay.com/features/features/team-member-roles/) [![](https://webhookrelay.com/features/images/features/teams/cover.png)

**Teams**

Create teams and invite colleagues to your Webhook Relay account so you can manage buckets, inputs, outputs and forwarding rules together, securely.](https://webhookrelay.com/features/features/teams/) [![](https://webhookrelay.com/features/images/features/throttling/cover.png)

**Webhook Throttling**

Smooth out webhook spikes before they reach your servers. Throttling paces delivery to a steady rate or fixed concurrency you choose.](https://webhookrelay.com/features/features/throttling/) [![](https://webhookrelay.com/features/images/docs/webhooks/tls/tls_settings.png)

**TLS Compatibility: Custom & Legacy TLS Versions**

Receive webhooks from legacy systems that can't speak modern TLS. Set a minimum TLS version per input, accept TLS 1.0/1.1, and skip verification per output.](https://webhookrelay.com/features/features/tls-compatibility/) [![](https://webhookrelay.com/features/images/features/transform-webhooks/cover.png)

**Serverless Webhook Transformations**

Transform, filter and reshape webhook payloads in flight with Webhook Relay functions before forwarding them to any destination — no extra infrastructure.](https://webhookrelay.com/features/features/transform-webhooks/) [![](https://webhookrelay.com/features/images/features/transform-with-ai/cover.png)

**Transform Webhooks with AI**

Automatically transform webhook payloads using AI with Webhook Relay — map fields, reformat data and integrate incompatible systems without writing parsers.](https://webhookrelay.com/features/features/transform-webhooks-with-ai/) [![](https://webhookrelay.com/features/images/features/kubernetes/cover.png)

**Webhook Relay Kubernetes Integration**

Seamlessly connect your Kubernetes services to external webhooks without exposing them directly to the internet using the Webhook Relay Operator.](https://webhookrelay.com/features/features/webhook-kubernetes-integration/) [![](https://webhookrelay.com/features/images/bucket_health.png)

**Webhook Logs & Delivery Monitoring**

See every webhook delivery, latency and failure in one place. Webhook Relay logs each event with its status and response, and lets you retry any delivery.](https://webhookrelay.com/features/features/webhook-logs/) [![](https://webhookrelay.com/features/images/features/multiple-destinations/cover.png)

**Forward Webhooks to Multiple Destinations**

Fan out a single webhook to multiple destinations with Webhook Relay — ideal for data replication, backups and sending events to several services at once.](https://webhookrelay.com/features/features/webhook-multiple-destinations/) [![](https://webhookrelay.com/features/images/features/webhook-to-internal-server/cover.png)

**Webhooks to Internal Servers**

Forward public webhooks to internal servers behind a firewall or NAT with Webhook Relay — no public IP, port forwarding or router changes required.](https://webhookrelay.com/features/features/webhook-to-internal-server/) ![Stripes](https://webhookrelay.com/features/images/stripes-dark.svg) ## **Start forwarding webhooks in minutes ** Connect a source, pick a destination, and Webhook Relay handles delivery, retries and transforms. Set up your first webhook in under five minutes. [Start for free ->](https://my.webhookrelay.com/register) Free plan · No credit card required · 7-day money-back guarantee on paid plans --- --- title: Node-RED | WebhookRelay meta: "og: title": Node-RED description: Directly receiving and process webhooks in Node-RED instance without public IP or domain. url: https://webhookrelay.com/docs/tutorials/edge/node-red.md file: /docs/tutorials/edge/node-red.md --- ![Stripes](https://webhookrelay.com/docs/tutorials/edge/node-red/images/stripes.svg) Documentation **Fundamentals** # **Node-RED** Directly receiving and process webhooks in Node-RED instance without public IP or domain. ## [What is Node RED?](#what-is-node-red) Node-RED is a visual programming tool that helps people connect and automate devices, services, and data in a simple and visual way. It's like a digital flowchart where you can create and link "nodes" to make things happen, such as turning on lights when you receive an email or collecting data from sensors and sending it to a database. Node-RED makes it easier for non-programmers to build custom automation and data-processing tasks without writing complex code. ## [Use case](#use-case) Webhook Relay websockets let devices to receive webhooks by popular services such as [IFTTT](https://ifttt.com/), [Zapier](https://zapier.com/) or anything else without having a public IP. It can also be used for remote access if you are using tunnels. Since webhooks are just a standard HTTP requests, any services can easily produce and consume them. Webhook Relay is particularly useful when: - Your IoT devices can't run an HTTP web server to receive webhooks - You don't want to run a public MQTT server - You cannot access your router to configure port forwarding - Router doesn't support port forwarding - Your ISP blocks inbound connections - You don't have a static IP address - Server that is hosting your home automation system is changing IP, location ## [Node-RED](#node-red) [Node-RED](https://nodered.org/) is a programming tool for wiring together hardware devices, APIs and online services in new and interesting ways. It provides a browser-based editor that makes it easy to wire together flows using the wide range of nodes in the palette that can be deployed to its runtime in a single-click. We provide [node-red-contrib-webhookrelay](https://www.npmjs.com/package/node-red-contrib-webhookrelay) node that can be used with Node-RED to easily received webhooks. ### [Usage](#usage) To use this node, some configuration is required: 1. Install `node-red-contrib-webhookrelay` 2. Create bucket at [https://my.webhookrelay.com/buckets](https://my.webhookrelay.com/buckets) 3. Generate tokens at [https://my.webhookrelay.com/tokens](https://my.webhookrelay.com/tokens) 4. Supply bucket and token key & secret into the node Detailed steps with screenshots are available below. ### [Installing node-red-contrib-webhookrelay node](#installing-node-red-contrib-webhookrelay-node) Open 'palette' on your Node-RED web interface and install `node-red-contrib-webhookrelay` node. ### [Creating Webhook Relay bucket](#creating-webhook-relay-bucket) Buckets are like groups where you can have multiple input URLs and multiple outputs. Since we are using WebSocket streaming, we don't really care about the outputs. However, it's good to create at least one output since then you will be able to resend webhooks manually (good for testing integrations). Go to your [buckets page](https://my.webhookrelay.com/buckets) and create a bucket called `nodered`. ### [Getting token key & secret](#getting-token-key-secret) Retrieve token key & secret from [https://my.webhookrelay.com/tokens](https://my.webhookrelay.com/tokens) page. While token key will remain visible, secret is already encrypted and cannot be decrypted. If you lose your secret, just delete the token and create a new one. ### [Configuring node-red-contrib-webhookrelay node](#configuring-node-red-contrib-webhookrelay-node) Add bucket name and your token key & secret into the node. ### [Receiving webhooks](#receiving-webhooks) Any HTTP requests that are received by your Wehbook Relay bucket will be sent to your Node-RED instance. ### [Message structure](#message-structure) Message contains several fields: - topic - bucket name so you can easily use switches if you have multiple buckets streaming at once. - payload - actual JSON object with all request information: ``` { "topic": "nodered", "payload": { "type": "webhook", "meta": { "bucked_id": "12302faf-43bd-43c4-ab1d-89a8b0505693", "bucket_name": "nodered", "input_id": "544a6fe8-83fe-4361-a264-0fd486e1665d", "input_name": "Default public endpoint", "output_name": "", "output_destination": "" }, "headers": { "Content-Type": ["application/json"], "Accept": ["*/*"], "Content-Length": ["29"], }, "query": "", "body": "{\n\t\"msg\": \"hello Node-RED!\"\n}", "method": "PUT" }, "_msgid": "eb4a7330.c838b" } ``` ### [Responding to webhooks](#responding-to-webhooks) Webhook Relay allows responding to webhooks via Node-RED from [0.3.0 version](https://www.npmjs.com/package/node-red-contrib-webhookrelay). To send responses, ensure that your bucket's input is configured to return responses (by default for security reasons it will always return 200 status code and an empty body): 1. Go to your buckets page [https://my.webhookrelay.com/buckets](https://my.webhookrelay.com/buckets) 2. Go to the bucket details 3. Go to the input details (click **CONFIGURE**) 4. From the 'Response configuration' section click on a 'Dynamic response from output' dropdown and select "Any output" Now, to send back responses from the Node-RED back to Webhook Relay so it can respond to the caller, form a payload: ``` return { meta: msg.payload.meta, // this is original meta field from the payload (it's important to include it so we have the message ID) status: 200, // status code to return (200, 201, 400, etc) body: "any payload here (if you want to send JSON, just stringify it first)", // body headers: { someheader: ['somevalue'] } }; ``` A simple flow that just responds to requests uses a function node feeding into the Webhook Relay node. Then, send this payload back to the Webhook Relay node through its input: ``` $ curl https://my.webhookrelay.com/v1/webhooks/d00e0b31-438f-454f-ab5e-406215aeef84 any payload here (if you want to send JSON, just stringify it first) ``` ### [Reporting issues](#reporting-issues) If you encounter any issues or requests, either submit them here or on the github repository here [https://github.com/webhookrelay/node-red-contrib-webhookrelay](https://github.com/webhookrelay/node-red-contrib-webhookrelay). Did this page help you? --- --- title: Webhook Relay - Thank you | WebhookRelay meta: "og: title": "Webhook Relay - Thank you" description: Thank you for your request. We will try to improve this! url: https://webhookrelay.com/thank-you.md file: /thank-you.md --- # **Thank you** Your request has been submitted. --- --- title: Home Assistant | WebhookRelay meta: "og: title": "Home Assistant" description: Connecting to your Home Assistant remotely without domain/public IP or configuring NAT. url: https://webhookrelay.com/docs/tutorials/edge/home-assistant.md file: /docs/tutorials/edge/home-assistant.md --- ![Stripes](https://webhookrelay.com/docs/tutorials/edge/home-assistant/images/stripes.svg) Documentation **Fundamentals** # **Home Assistant** Connecting to your Home Assistant remotely without domain/public IP or configuring NAT. ## [How does it work?](#how-does-it-work) Webhook Relay for home automation addons work exactly the same as our **CLI** or **webhookrelayd** tunneling daemon. These local agents create a reverse tunnel back to the [https://my.webhookrelay.com](https://my.webhookrelay.com) cloud service. Any HTTP requests received by the public endpoints will be routed to your private endpoints. Webhook Relay provides end-to-end encryption, both tunnel and public endpoints use HTTPS for webhook forwarding. If you are using tunnels, HTTPS is optional (defaults to enabled) for all paid plans. ## [What's the use case?](#whats-the-use-case) Webhook Relay addon helps to receive webhooks by popular services such as [IFTTT](https://ifttt.com/) or [Zapier](https://zapier.com/) and relay them to your Home Assistant or Node-RED instances. It can also be used for remote access if you are using tunnels. Since webhooks are just a standard HTTP requests, any services can easily produce and consume them. Webhook Relay is particularly useful when: - You cannot access your router to configure port forwarding - Router doesn't support port forwarding - Your ISP blocks inbound connections - You don't have a static IP address - Server that is hosting your home automation system is changing IP, location ### [You should use webhooks forwarding when:](#you-should-use-webhooks-forwarding-when) - Service that is sending requests to your home automation instance doesn't expect responses (usually webhook producers don't expect anything) - Additional security is required for your server and you don't want to expose it to the internet. Webhooks producer won't get any information about the server that is consuming your webhooks ### [You should use tunnels when:](#you-should-use-tunnels-when) - You need remote access to your home automation instance (for example you want to view it through the browser). - Service that is calling your home automation instance wants to receive responses from it. ## [Security best practices](#security-best-practices) Like with any technology, some knowledge about Webhook Relay offered features is required. First of all, applications usually set cookies or JWT tokens. It is important to keep this information secure and you should not use HTTP (non-HTTPS) tunnels for this. Make sure: - You use TLS pass-through tunnels with your own certificates (add-on can generate Let's Encrypt certificates for DuckDNS or generate self-signed ones). - You expose only those services that need to be exposed. Use webhook forwarding functionality when the server doesn't have to respond (for example you are receiving webhooks and don't need remote access via your browser). ## [Home Assistant](#home-assistant) Webhook Relay provides a secure, stripped down tunneling daemon `webhookrelayd` which can be used as a Home Assistant add-on. ![Webhook Relay Home Assistant Add-on](https://webhookrelay.com/docs/tutorials/edge/home-assistant/images/blog/hassio-addon/ha-duckdns.png) ## [Installation](#installation) There are several configuration options for this add-on. You can always mix and match them (Simple HTTPS, DuckDNS, Cloudflare). Installation instructions can be found [here](https://docs.webhookrelay.com/short-examples/home-automation/home-assistant). Most popular configuration uses [DuckDNS](https://docs.webhookrelay.com/short-examples/home-automation/home-assistant#option-tls-pass-through-with-duckdns) as it provides you with your own free subdomain that can be used to retrieve TLS certificates. **Cloudflare integration** If you have your own domain name, you can follow instructions [in the Cloudflare section](https://docs.webhookrelay.com/short-examples/home-automation/home-assistant#option-tls-pass-through-with-cloudflare) to start using it. ![Home Assistant Cloudflare](https://webhookrelay.com/docs/tutorials/edge/home-assistant/images/ha-cloudflare.png) On top of it allowing you to have any domain names with TLS pass-through tunnels, you get some additional benefits: - Minification - Remove unwanted characters like whitespaces, comments, new line characters, block delimiters which are not needed for a web page to serve. - Cloud WAF - WAF (Web Application Firewall) help to keep your site secure from OWASP top 10, CMS (WordPress, Joomla, etc. ) vulnerabilities. Cloudflare WAF got more than 145 rules to protect from almost all types of web applications attack. - Browser Caching - Optimized Network Routing ## [Issue reporting & support](#issue-reporting-support) If you have any questions or have encountered an issue. Please check Webhook Relay addon logs and supply them here [https://github.com/webhookrelay/home-assistant/issues](https://github.com/webhookrelay/home-assistant/issues) or email us at [support@webhookrelay.com](https://webhookrelay.com/docs/tutorials/edge/home-assistant//mailto:support@webhookrelay.com) ## [FAQ](#faq) **Q: Does using Webhook Relay to forward webhooks makes my Home Assistant instance less secure?** A: Using our service makes your Home Assistant **more** secure, as webhook forwarding is one-way traffic only and no information about your Home Assistant can be retrieved. **Q: Is free plan enough for me?** A: Depends on your usage. If you just want to relay webhooks to your internal Home Assistant server then using free tier should be enough, current limit is 150 webhooks per month. If you want to access it remotely via tunnels, we would recommend to subscribe to a basic plan which is just 4.5$ per month and get secure HTTPS tunnels. **Q: Why are webhooks recorded?** A: Webhook Relay is used by engineers and developers to develop, debug and proxy various webhook requests to other services. Recording enables you to inspect the traffic. Only you or your sub-accounts can access them. Usually webhooks don't store any sensitive information. **Q: Is tunnel traffic recorded?** A: No, you can view our [GDPR](https://webhookrelay.com/docs/tutorials/edge/home-assistant/gdpr/) policy. Tunnel traffic is not recorded. Also, please use TLS tunnels whenever possible for maximum protection. **Q: Do phone push notifications work with the tunnels?** A: Push notifications are based on HTTP2 standard that requires TLS all the way. It will work if Home Assistant has TLS enabled (you would be accessing it locally over `https://192.168.*.*:8123`). To do that, you can generate certificates yourself and just supply in the tunnel to not do TLS termination. Example configuration: ``` { "key": "[YOUR TOKEN KEY]", "secret": "[YOUR TOKEN SECRET]", "forwarding": [], "tunnels": [ { "name": "home-assistant", "destination": "https://127.0.0.1:8123", "protocol": "tls", "subdomain": "[YOUR SUBDOMAIN]", } ], "cloudflare": { "email": "", "api_key": "" }, "duck_dns": { "token": "", "accept_terms": false }, "tunnels_enabled": true, "forwarding_enabled": false } ``` Did this page help you? --- --- title: Free DocuSign Webhook Signature Verifier | WebhookRelay meta: "og: title": "Free DocuSign Webhook Signature Verifier" description: Free DocuSign webhook signature verifier. Validate the X-DocuSign-Signature-1 header against your Connect HMAC key. No signup, runs in your browser. url: https://webhookrelay.com/verify-docusign-webhook-signature.md file: /verify-docusign-webhook-signature.md --- # **Verify DocuSign Webhook Signatures** DocuSign Connect signs each webhook with HMAC-SHA256 over the exact raw payload bytes, base64-encodes the digest and sends it in the `X-DocuSign-Signature-1` header (with `-2`, `-3`… when several HMAC keys are active). Paste the raw body, your Connect HMAC key and the header value below. **Raw request body (payload)** **Connect HMAC secret key** **Computed signature** **Signature to verify **(X-DocuSign-Signature-1) **Paste the X-DocuSign-Signature-1 value above to compare** Everything runs in your browser — the payload and secret never leave this page. Want to verify a different provider? See the [webhook signature verifier hub](https://webhookrelay.com/verify-docusign-webhook-signature/verify-webhook-signature/) or the generic [HMAC generator](https://webhookrelay.com/verify-docusign-webhook-signature/hmac-verification/). ## **How DocuSign signs webhooks** 1. Enable HMAC security on your Connect configuration and store the generated key — DocuSign shows it only once. 2. On each delivery, read `X-DocuSign-Signature-1` (one header per active key: `-2`, `-3`… up to 100). 3. Compute HMAC-SHA256 of the **raw** request body using the key, then base64-encode the digest. 4. Compare against the header with a constant-time check. A match against any active key's header authenticates the delivery. **References & official docs:** - [DocuSign — HMAC security for Connect](https://developers.docusign.com/platform/webhooks/connect/hmac/) - [DocuSign — How to validate an HMAC signature](https://developers.docusign.com/platform/webhooks/connect/validate/) ## **Verify DocuSign signatures in code** **Node.js** ``` const crypto = require('crypto'); // payload must be the RAW request body bytes DocuSign sent. function isValidDocuSignWebhook(payload, signature, hmacKey) { const expected = crypto .createHmac('sha256', hmacKey) .update(payload) .digest('base64'); return crypto.timingSafeEqual( Buffer.from(expected), Buffer.from(signature)); } const ok = isValidDocuSignWebhook( rawBody, req.headers['x-docusign-signature-1'], process.env.DOCUSIGN_HMAC_KEY); ``` **Python** ``` import base64, hashlib, hmac def is_valid_docusign_webhook(payload: bytes, signature: str, hmac_key: str) -> bool: digest = hmac.new(hmac_key.encode(), payload, hashlib.sha256).digest() expected = base64.b64encode(digest).decode() return hmac.compare_digest(expected, signature) ``` ## **Frequently asked questions**
**Are DocuSign Connect webhooks signed by default?** No. Until you enable HMAC on the Connect configuration and add at least one key, deliveries carry no `X-DocuSign-Signature-*` headers. Enable it in the eSignature admin under Connect → your configuration.
**Why are there multiple X-DocuSign-Signature-N headers?** One per active HMAC key (up to 100), which lets you rotate keys with zero downtime: verify against each active key and accept if any matches.
**Does DocuSign retry failed webhook deliveries?** Yes, aggressively — Connect keeps retrying for around 24 hours with growing intervals. We have observed envelopes arriving with `retryCount` above 20, so make your handler idempotent and return 2xx quickly.
## **Verify other providers** [![Stripe logo](https://webhookrelay.com/verify-docusign-webhook-signature/images/landing/logos/stripe.svg)Stripe](https://webhookrelay.com/verify-docusign-webhook-signature/verify-stripe-webhook-signature/) [![GitHub logo](https://webhookrelay.com/verify-docusign-webhook-signature/images/landing/logos/github.svg) GitHub](https://webhookrelay.com/verify-docusign-webhook-signature/verify-github-webhook-signature/) [![Shopify logo](https://webhookrelay.com/verify-docusign-webhook-signature/images/landing/logos/shopify.svg) Shopify](https://webhookrelay.com/verify-docusign-webhook-signature/verify-shopify-webhook-signature/) [![Slack logo](https://webhookrelay.com/verify-docusign-webhook-signature/images/landing/logos/slack.svg) Slack](https://webhookrelay.com/verify-docusign-webhook-signature/verify-slack-webhook-signature/) [![Twilio logo](https://webhookrelay.com/verify-docusign-webhook-signature/images/landing/logos/twilio.svg) Twilio](https://webhookrelay.com/verify-docusign-webhook-signature/verify-twilio-webhook-signature/) [**S** Standard Webhooks](https://webhookrelay.com/verify-docusign-webhook-signature/verify-standard-webhooks-signature/) [![Square logo](https://webhookrelay.com/verify-docusign-webhook-signature/images/landing/logos/square.svg) Square](https://webhookrelay.com/verify-docusign-webhook-signature/verify-square-webhook-signature/) [**L** LINE](https://webhookrelay.com/verify-docusign-webhook-signature/verify-line-webhook-signature/) [**A** Airwallex](https://webhookrelay.com/verify-docusign-webhook-signature/verify-airwallex-webhook-signature/) [**M** MyFatoorah](https://webhookrelay.com/verify-docusign-webhook-signature/verify-myfatoorah-webhook-signature/) [![Zendesk logo](https://webhookrelay.com/verify-docusign-webhook-signature/images/landing/logos/zendesk.svg) Zendesk](https://webhookrelay.com/verify-docusign-webhook-signature/verify-zendesk-webhook-signature/) [**K** Klarna](https://webhookrelay.com/verify-docusign-webhook-signature/verify-klarna-webhook-signature/) [**A** Attio](https://webhookrelay.com/verify-docusign-webhook-signature/verify-attio-webhook-signature/) [HMAC Generator](https://webhookrelay.com/verify-docusign-webhook-signature/hmac-verification/) [CloudEvents Validator](https://webhookrelay.com/verify-docusign-webhook-signature/cloudevents-validator/) [Webhook Tester (Bin)](https://webhookrelay.com/verify-docusign-webhook-signature/webhook-bin/) Receiving DocuSign webhooks on a server behind a firewall or on localhost? Webhook Relay can [forward them to your internal service](https://webhookrelay.com/verify-docusign-webhook-signature/webhooks/) and even [verify or transform them](https://webhookrelay.com/verify-docusign-webhook-signature/features/transform-webhooks/) before delivery. --- --- title: Durable Webhook Retries: A Live Demo | WebhookRelay meta: "og: title": "Durable Webhook Retries: A Live Demo" description: We launched durable retries — webhooks persisted and retried with backoff for up to 30 days. We built an endpoint that fails 80% of the time to prove it. url: https://webhookrelay.com/blog/durable-webhook-retries-demo.md file: /blog/durable-webhook-retries-demo.md --- ![Stripes](https://webhookrelay.com/blog/durable-webhook-retries-demo/images/stripes.svg) # **Durable webhook retries: a live demo of webhooks that never give up** We launched durable retries — webhooks persisted and retried with backoff for up to 30 days. We built an endpoint that fails 80% of the time to prove it. **A webhook you don't receive might as well have never happened.** A deploy, a restart, a database hiccup, a five-minute outage — miss the delivery and the event is usually gone for good. Most providers retry a handful of times over a few minutes, then give up. Today we're launching [**durable retries**](https://webhookrelay.com/blog/durable-webhook-retries-demo/features/durable-retries/): turn it on for any destination and Webhook Relay takes responsibility for getting the event there. Every webhook is saved to durable storage the moment it arrives, and if the first attempts fail, delivery doesn't stop — it backs off and keeps trying, all the way out to **30 days**. Talking about reliability is easy, though. So we built something that fails on purpose and watched durable retries win anyway. ## [Meet flakey-script: an endpoint that fails 80% of the time](#meet-flakey-script-an-endpoint-that-fails-80-of-the-time) [**flakey-script**](https://github.com/webhookrelay/flakey-script) is a tiny, open-source Node.js webhook receiver with one job: be unreliable in a realistic way. Send it an event like this: ``` { "type": "event", "id": "xx-123", "data": "test" } ``` and for each unique `id` it behaves like a real endpoint having a bad day: - While an `id` is **younger than an hour**, it returns `500` about **80% of the time** (and `200` the rest — the occasional lucky success). - Once that `id` is **at least an hour old**, it **always** returns `200` — the "endpoint has recovered" moment. - Once an `id` has succeeded, every later retry for it is an **idempotent `200`**: it's never processed twice. State lives on the filesystem, so it survives restarts and you can watch every `id` converge on _delivered_ over time. The whole thing is a couple hundred lines of dependency-free Node — [read it on GitHub](https://github.com/webhookrelay/flakey-script). ## [The setup](#the-setup) The demo wires flakey-script up to Webhook Relay using a [tunnel](https://webhookrelay.com/blog/durable-webhook-retries-demo/tunnels/) so the local app is reachable as a public destination — no inbound ports, no public IP: ``` sender ──▶ Webhook Relay ──(durable retries)──▶ tunnel ──▶ flakey-script ├─ ~80% of early attempts → 500 └─ same id, 1h later → 200 (always) ``` It runs as a two-container `docker compose` stack — the app plus the Webhook Relay agent that connects the tunnel: 1. Create a bucket (`durable-demo`) with a public endpoint to receive webhooks. 2. Create a tunnel pointing at `http://flakey-script:3000` and run the agent in compose. 3. Add a **public** output to the bucket, set its destination to the tunnel URL, and switch on **durable delivery** with the **Medium (~16 h)** schedule. 4. Fire a batch of 25 webhooks at the bucket and walk away. That's it. Everything after step 4 is Webhook Relay's problem now. ## [Watching it converge](#watching-it-converge) Within seconds, the request log fills with attempts. Each delivery shows its status: **`sent`** when the endpoint returns `2xx`, **`stalled`** when it's failed so far and is waiting for the next retry. The fast-retry phase does most of the work immediately — a single `POST` becomes a flurry of attempts, and the lucky ones land right away. Here's the receiver's own dashboard a few minutes in: 25 events received, **20 delivered**, 5 still retrying, and **98 total delivery attempts** behind the scenes. The five stragglers are the unlucky ones — the events that kept rolling 80% failures. They don't get dropped. After the 15-minute handoff window they move to the durable retry engine, which keeps trying on an exponential-backoff schedule. Each one is **guaranteed** to land on its next retry after it crosses the one-hour mark. No manual reconciliation, no lost events — the whole batch converges on _delivered_. ![Durable retries with exponential backoff over time](https://webhookrelay.com/blog/durable-webhook-retries-demo/images/features/durable-retries/retry-schedule.png) Each retry waits a little longer than the last, so a struggling server gets room to recover instead of being hammered while it's already down. ### [What convergence actually looks like](#what-convergence-actually-looks-like) Here's the same story as a timeline you can explore. Press **play** (or drag the slider) to watch 50 webhooks move from _retrying_ to _delivered_ over an hour and a bit, and flip to **Retry attempts** to see the delivery effort behind the curve. Hover anywhere for the exact numbers. Durable delivery · live convergence 50/ 50 delivered 100%at 95 min 0 25 50 0m 15m 30m 45m 60m 75m 90m handoff 1-hour guarantee DeliveredRetryingAttempts50 webhooks · 275 attempts · 100% by ~73 min Three phases stand out, and they're the same three you'll see in your own dashboard: 1. **The fast-retry burst (0–15 min).** The first attempt becomes a flurry of attempts, and most events land almost immediately — about half are delivered within the first couple of minutes. This is where the bulk of the work happens, with no delay you'd notice. 2. **The stubborn tail (15–60 min).** A handful of events keep rolling failures. Instead of dropping them, Webhook Relay hands them to durable retry and keeps them queued with growing backoff — the _retrying_ band thins out slowly. 3. **The one-hour guarantee (60 min+).** As each remaining event crosses the recovery threshold, its next retry finally lands. The curve closes to 100%: every webhook delivered, nothing reconciled by hand. That last phase is the whole point of durability — the long tail of "impossible" deliveries that a few minutes of provider retries would have lost forever. ### [The best part: turn it off](#the-best-part-turn-it-off) Here's the test that really makes the point. Stop the receiver entirely — close the laptop, kill the containers, go to lunch. To Webhook Relay that's just another outage. The events sit safely in durable storage, and the retries you'd otherwise have lost are still queued. Start everything back up and, once the events are old enough, they all land. The "downtime" became a non-event. ## [Why this matters](#why-this-matters) The durability you'd normally bolt together from a queue, a database and a cron job — done for you, per destination, with a single switch: - **Payments and billing.** A processor or your billing service goes down for an hour. The confirmations wait, then deliver. - **Deploys without dropped events.** Ship in the middle of the day; webhooks that arrive during the restart queue up and land when your service is healthy again. - **Flaky partner endpoints.** Integrating with a service that returns 500s under load? Let it fail and recover on its own schedule instead of writing your own retry plumbing. - **Internal endpoints.** Durable retries cover [services behind your firewall](https://webhookrelay.com/blog/durable-webhook-retries-demo/features/webhook-to-internal-server/) too — even ones that were offline when the event arrived. ## [One thing to remember: be idempotent](#one-thing-to-remember-be-idempotent) Because durable delivery is **at-least-once**, the same event can legitimately arrive more than once (your endpoint processed it but the `200` got lost on the way back, so we retry). That's exactly why flakey-script keys on the event `id` and treats a repeat as a no-op. Do the same in your handler: deduplicate on a stable id, return `2xx` once you've safely accepted an event, and a `5xx` to ask for a retry. The [durable webhooks guide](https://webhookrelay.com/blog/durable-webhook-retries-demo/docs/webhooks/durable-webhooks/) covers the pattern in detail. ## [Try it yourself](#try-it-yourself) - ⭐ **Run the demo:** [github.com/webhookrelay/flakey-script](https://github.com/webhookrelay/flakey-script) - 📖 **Read the guide:** [Durable webhooks: reliable delivery with automatic retries](https://webhookrelay.com/blog/durable-webhook-retries-demo/docs/webhooks/durable-webhooks/) - 🧩 **Feature overview:** [Durable retries](https://webhookrelay.com/blog/durable-webhook-retries-demo/features/durable-retries/) Ready to make sure no webhook slips through? [Create a free account](https://my.webhookrelay.com/register) and turn on durable retries for your first destination. --- --- title: Free Klarna Webhook Signature Verifier | WebhookRelay meta: "og: title": "Free Klarna Webhook Signature Verifier" description: Free Klarna webhook signature verifier. Validate the Klarna-Signature header against your notification signing key. No signup, runs in your browser. url: https://webhookrelay.com/verify-klarna-webhook-signature.md file: /verify-klarna-webhook-signature.md --- # **Verify Klarna Webhook Signatures** Klarna signs webhook notifications with HMAC-SHA256 over the raw JSON body using a signing key you generate when registering the webhook, and sends the hex digest in the `Klarna-Signature` header. The `Klarna-Signing-Key-Id` header tells you which key was used. Paste the raw body, the signing key and the signature below. **Raw request body (payload)** **Webhook signing key** **Computed signature** **Signature to verify **(Klarna-Signature) **Paste the Klarna-Signature value above to compare** Everything runs in your browser — the payload and secret never leave this page. Want to verify a different provider? See the [webhook signature verifier hub](https://webhookrelay.com/verify-klarna-webhook-signature/verify-webhook-signature/) or the generic [HMAC generator](https://webhookrelay.com/verify-klarna-webhook-signature/hmac-verification/). ## **How Klarna signs webhooks** 1. Read `Klarna-Signing-Key-Id` from the request and look up the matching signing key you stored at registration time. 2. Compute HMAC-SHA256 of the **raw** JSON request body using that signing key. 3. Hex-encode the digest and compare it to `Klarna-Signature` with a constant-time check. 4. On mismatch, reject with `400`; keep old keys available during rotation so in-flight notifications still verify. **References & official docs:** - [Klarna — Webhooks registration and signing](https://docs.klarna.com/acquirer/klarna/web-payments/additional-resources/webhooks-registration/) ## **Verify Klarna signatures in code** **Node.js** ``` const crypto = require('crypto'); // Look up the signing key by the Klarna-Signing-Key-Id header. function isValidKlarnaWebhook(rawBody, signature, signingKey) { const expected = crypto .createHmac('sha256', signingKey) .update(rawBody) .digest('hex'); return crypto.timingSafeEqual( Buffer.from(expected), Buffer.from(signature)); } ``` **Python** ``` import hashlib, hmac def is_valid_klarna_webhook(raw_body: bytes, signature: str, signing_key: str) -> bool: expected = hmac.new(signing_key.encode(), raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, signature) ``` ## **Frequently asked questions**
**What is the Klarna-Signing-Key-Id header for?** It identifies which signing key produced the signature (keys look like `krn:partner:global:notification:signing-key:…`). Store the key id → key mapping when you register the webhook and use it to pick the right key before verifying.
**Can I retrieve a lost signing key?** No — Klarna shows the signing key only when you generate it. If it is lost, generate a new key and update your verifier; support both keys briefly so in-flight notifications still pass.
**What should my endpoint return?** Acknowledge quickly with 200/201/202/204 — do the real work asynchronously. Klarna retries failed deliveries, so handlers must be idempotent.
## **Verify other providers** [![Stripe logo](https://webhookrelay.com/verify-klarna-webhook-signature/images/landing/logos/stripe.svg)Stripe](https://webhookrelay.com/verify-klarna-webhook-signature/verify-stripe-webhook-signature/) [![GitHub logo](https://webhookrelay.com/verify-klarna-webhook-signature/images/landing/logos/github.svg) GitHub](https://webhookrelay.com/verify-klarna-webhook-signature/verify-github-webhook-signature/) [![Shopify logo](https://webhookrelay.com/verify-klarna-webhook-signature/images/landing/logos/shopify.svg) Shopify](https://webhookrelay.com/verify-klarna-webhook-signature/verify-shopify-webhook-signature/) [![Slack logo](https://webhookrelay.com/verify-klarna-webhook-signature/images/landing/logos/slack.svg) Slack](https://webhookrelay.com/verify-klarna-webhook-signature/verify-slack-webhook-signature/) [![Twilio logo](https://webhookrelay.com/verify-klarna-webhook-signature/images/landing/logos/twilio.svg) Twilio](https://webhookrelay.com/verify-klarna-webhook-signature/verify-twilio-webhook-signature/) [**S** Standard Webhooks](https://webhookrelay.com/verify-klarna-webhook-signature/verify-standard-webhooks-signature/) [![Square logo](https://webhookrelay.com/verify-klarna-webhook-signature/images/landing/logos/square.svg) Square](https://webhookrelay.com/verify-klarna-webhook-signature/verify-square-webhook-signature/) [**L** LINE](https://webhookrelay.com/verify-klarna-webhook-signature/verify-line-webhook-signature/) [**A** Airwallex](https://webhookrelay.com/verify-klarna-webhook-signature/verify-airwallex-webhook-signature/) [**M** MyFatoorah](https://webhookrelay.com/verify-klarna-webhook-signature/verify-myfatoorah-webhook-signature/) [![Zendesk logo](https://webhookrelay.com/verify-klarna-webhook-signature/images/landing/logos/zendesk.svg) Zendesk](https://webhookrelay.com/verify-klarna-webhook-signature/verify-zendesk-webhook-signature/) [![DocuSign logo](https://webhookrelay.com/verify-klarna-webhook-signature/images/landing/logos/docusign.svg) DocuSign](https://webhookrelay.com/verify-klarna-webhook-signature/verify-docusign-webhook-signature/) [**A** Attio](https://webhookrelay.com/verify-klarna-webhook-signature/verify-attio-webhook-signature/) [HMAC Generator](https://webhookrelay.com/verify-klarna-webhook-signature/hmac-verification/) [CloudEvents Validator](https://webhookrelay.com/verify-klarna-webhook-signature/cloudevents-validator/) [Webhook Tester (Bin)](https://webhookrelay.com/verify-klarna-webhook-signature/webhook-bin/) Receiving Klarna webhooks on a server behind a firewall or on localhost? Webhook Relay can [forward them to your internal service](https://webhookrelay.com/verify-klarna-webhook-signature/webhooks/) and even [verify or transform them](https://webhookrelay.com/verify-klarna-webhook-signature/features/transform-webhooks/) before delivery. --- --- title: Free Airwallex Webhook Signature Verifier | WebhookRelay meta: "og: title": "Free Airwallex Webhook Signature Verifier" description: Free Airwallex webhook signature verifier. Validate the x-signature header against your webhook secret and x-timestamp. No signup, runs in your browser. url: https://webhookrelay.com/verify-airwallex-webhook-signature.md file: /verify-airwallex-webhook-signature.md --- # **Verify Airwallex Webhook Signatures** Airwallex signs each webhook by concatenating the `x-timestamp` value with the raw request body and computing HMAC-SHA256 with your endpoint's webhook secret. The **hex** result is sent in the `x-signature` header. Paste the raw body, the timestamp, your secret and the signature. **Raw request body (payload)** **Timestamp** (x-timestamp) **Webhook secret key** **Computed signature** **Signature to verify **(x-signature) **Paste the x-signature value above to compare** Everything runs in your browser — the payload and secret never leave this page. Want to verify a different provider? See the [webhook signature verifier hub](https://webhookrelay.com/verify-airwallex-webhook-signature/verify-webhook-signature/) or the generic [HMAC generator](https://webhookrelay.com/verify-airwallex-webhook-signature/hmac-verification/). ## **How Airwallex signs webhooks** 1. Read the `x-timestamp` header (a Unix time in **milliseconds**). 2. Build the signed value by concatenating the timestamp directly with the **raw** request body: timestamp first, body second, no separator. 3. Compute HMAC-SHA256 of that value using your endpoint's webhook secret as the key, hex-encoded. 4. Constant-time compare it to `x-signature`, and reject events whose `x-timestamp` is too old to block replays. **References & official docs:** - [Airwallex — Listen for webhook events (check signatures)](https://www.airwallex.com/docs/developer-tools/webhooks/listen-for-webhook-events) - [Airwallex — Webhook code examples](https://www.airwallex.com/docs/developer-tools/webhooks/listen-for-webhook-events/code-examples) ## **Verify Airwallex signatures in code** **Node.js** ``` const crypto = require('crypto'); const ts = req.headers['x-timestamp']; const expected = crypto .createHmac('sha256', process.env.AIRWALLEX_WEBHOOK_SECRET) .update(ts + rawBody) // timestamp + raw body, no separator .digest('hex'); const valid = crypto.timingSafeEqual( Buffer.from(expected), Buffer.from(req.headers['x-signature'])); ``` **Python** ``` import hmac, hashlib ts = request.headers['x-timestamp'] expected = hmac.new( webhook_secret.encode(), ts.encode() + raw_body, hashlib.sha256).hexdigest() valid = hmac.compare_digest(expected, request.headers['x-signature']) ``` ## **Frequently asked questions**
**Which secret signs Airwallex webhooks?** The webhook secret shown when you create the webhook (notification URL) in the Airwallex web app. Each endpoint has its own secret and it is used as a raw string — it is not your API key or client ID.
**Is x-timestamp in seconds or milliseconds?** Milliseconds since the Unix epoch. Compare it against the current time in milliseconds and reject events outside your tolerance. Airwallex does not mandate a window; a few minutes is a sensible default.
**Why does verification fail with the correct secret?** Usually the body was re-serialized. Airwallex signs the exact raw bytes, so verify against the unparsed request body and prepend the timestamp with no separator (`x-timestamp + body`).
## **Verify other providers** [![Stripe logo](https://webhookrelay.com/verify-airwallex-webhook-signature/images/landing/logos/stripe.svg)Stripe](https://webhookrelay.com/verify-airwallex-webhook-signature/verify-stripe-webhook-signature/) [![GitHub logo](https://webhookrelay.com/verify-airwallex-webhook-signature/images/landing/logos/github.svg) GitHub](https://webhookrelay.com/verify-airwallex-webhook-signature/verify-github-webhook-signature/) [![Shopify logo](https://webhookrelay.com/verify-airwallex-webhook-signature/images/landing/logos/shopify.svg) Shopify](https://webhookrelay.com/verify-airwallex-webhook-signature/verify-shopify-webhook-signature/) [![Slack logo](https://webhookrelay.com/verify-airwallex-webhook-signature/images/landing/logos/slack.svg) Slack](https://webhookrelay.com/verify-airwallex-webhook-signature/verify-slack-webhook-signature/) [![Twilio logo](https://webhookrelay.com/verify-airwallex-webhook-signature/images/landing/logos/twilio.svg) Twilio](https://webhookrelay.com/verify-airwallex-webhook-signature/verify-twilio-webhook-signature/) [**S** Standard Webhooks](https://webhookrelay.com/verify-airwallex-webhook-signature/verify-standard-webhooks-signature/) [![Square logo](https://webhookrelay.com/verify-airwallex-webhook-signature/images/landing/logos/square.svg) Square](https://webhookrelay.com/verify-airwallex-webhook-signature/verify-square-webhook-signature/) [**L** LINE](https://webhookrelay.com/verify-airwallex-webhook-signature/verify-line-webhook-signature/) [**M** MyFatoorah](https://webhookrelay.com/verify-airwallex-webhook-signature/verify-myfatoorah-webhook-signature/) [![Zendesk logo](https://webhookrelay.com/verify-airwallex-webhook-signature/images/landing/logos/zendesk.svg) Zendesk](https://webhookrelay.com/verify-airwallex-webhook-signature/verify-zendesk-webhook-signature/) [![DocuSign logo](https://webhookrelay.com/verify-airwallex-webhook-signature/images/landing/logos/docusign.svg) DocuSign](https://webhookrelay.com/verify-airwallex-webhook-signature/verify-docusign-webhook-signature/) [**K** Klarna](https://webhookrelay.com/verify-airwallex-webhook-signature/verify-klarna-webhook-signature/) [**A** Attio](https://webhookrelay.com/verify-airwallex-webhook-signature/verify-attio-webhook-signature/) [HMAC Generator](https://webhookrelay.com/verify-airwallex-webhook-signature/hmac-verification/) [CloudEvents Validator](https://webhookrelay.com/verify-airwallex-webhook-signature/cloudevents-validator/) [Webhook Tester (Bin)](https://webhookrelay.com/verify-airwallex-webhook-signature/webhook-bin/) Receiving Airwallex webhooks on a server behind a firewall or on localhost? Webhook Relay can [forward them to your internal service](https://webhookrelay.com/verify-airwallex-webhook-signature/webhooks/) and even [verify or transform them](https://webhookrelay.com/verify-airwallex-webhook-signature/features/transform-webhooks/) before delivery. --- --- title: JavaScript app | WebhookRelay meta: "og: title": "JavaScript app" description: Receive webhooks directly inside your application without public IP url: https://webhookrelay.com/docs/tutorials/edge/javascript-app.md file: /docs/tutorials/edge/javascript-app.md --- ![Stripes](https://webhookrelay.com/docs/tutorials/edge/javascript-app/images/stripes.svg) Documentation **Fundamentals** # **JavaScript app** Receive webhooks directly inside your application without public IP Webhook Relay's [Socket Server](https://docs.webhookrelay.com/products/webhooks/websocket-server) allows users to receive webhooks inside their application without having public IP, domain or even running a web server themselves. Here's a short example application written in JavaScript that subscribes to a stream of webhooks: ## [The code](#the-code) ``` // client.js const WebSocket = require('ws'); const ws = new WebSocket('wss://my.webhookrelay.com/v1/socket'); var apiKey = process.env.RELAY_KEY; var apiSecret = process.env.RELAY_SECRET;![](2023-08-27-22-40-21.png) ws.on('open', function open() { // on connection, send our authentication request ws.send(JSON.stringify({action: 'auth', key: apiKey, secret: apiSecret})); }); ws.on('close', function close() { console.log('disconnected'); }); ws.on('message', function incoming(data) { console.log(data) var msg = JSON.parse(data); if (msg.type === 'status' && msg.status === 'authenticated') { // if we got authentication confirmation, send subscribe event to the server ws.send(JSON.stringify({action: 'subscribe', buckets: ['123']})); } }); ``` ## [Install websocket library](#install-websocket-library) We will use the `ws` library for websocket connectivity. It's extremely popular ([https://www.npmjs.com/package/ws](https://www.npmjs.com/package/ws)) and should have a decent support: ``` npm i ws ``` ## [Set token key and secret](#set-token-key-and-secret) Generate your access key and secret in the [tokens page](https://my.webhookrelay.com/tokens) ``` export RELAY_KEY=your-token-key export RELAY_SECRET=your-token-secret ``` ## [Start the application](#start-the-application) To start it, use `node` directly: ``` node client.js ``` Now, if you send a webhook to your public input endpoint, you should see something similar: ``` $ node client.js {"type":"status","status":"authenticated","message":"connected successfully, subscribe to buckets"} {"type":"status","status":"subscribed","message":"subscribed to buckets: 123"} {"type":"webhook","meta":{"bucked_id":"89e44c32-27ff-4832-8655-8a42d3851b6f","bucket_name":"123","input_id":"ee4ac550-12a4-41a7-837d-dd3356ed1771","input_name":"Default public endpoint"},"headers":{"Content-Length":["15"],"User-Agent":["insomnia/6.0.2"],"Cookie":["__cfduid=dc244a014f0b1e2965544ddb483c3fe1b1525866866"],"Content-Type":["application/json"],"Accept":["*/*"]},"query":"","body":"{\"hi\": \"there\"}","method":"PUT"} ``` Did this page help you? --- --- title: Free Attio Webhook Signature Verifier | WebhookRelay meta: "og: title": "Free Attio Webhook Signature Verifier" description: Free Attio webhook signature verifier. Validate the Attio-Signature header against your webhook secret. No signup, runs in your browser. url: https://webhookrelay.com/verify-attio-webhook-signature.md file: /verify-attio-webhook-signature.md --- # **Verify Attio Webhook Signatures** Attio signs each webhook with HMAC-SHA256 over the request body using the webhook's secret, and sends the hex digest in the `Attio-Signature` header. Paste the raw body, the secret from your developer settings and the signature below. **Raw request body (payload)** **Webhook secret** **Computed signature** **Signature to verify **(Attio-Signature) **Paste the Attio-Signature value above to compare** Everything runs in your browser — the payload and secret never leave this page. Want to verify a different provider? See the [webhook signature verifier hub](https://webhookrelay.com/verify-attio-webhook-signature/verify-webhook-signature/) or the generic [HMAC generator](https://webhookrelay.com/verify-attio-webhook-signature/hmac-verification/). ## **How Attio signs webhooks** 1. Get the webhook's secret from the Attio developer settings page (also returned by the API when the webhook is created). 2. Compute HMAC-SHA256 of the **raw** request body using the secret as the key. 3. Hex-encode the digest and compare it to `Attio-Signature` with a constant-time comparison. 4. Prefer signature verification over IP allowlisting — it keeps working when Attio adds new egress IPs. **References & official docs:** - [Attio — Configuring webhooks](https://docs.attio.com/rest-api/guides/webhooks) ## **Verify Attio signatures in code** **Node.js** ``` const crypto = require('crypto'); function isValidAttioWebhook(rawBody, signature, secret) { const expected = crypto .createHmac('sha256', secret) .update(rawBody) .digest('hex'); return crypto.timingSafeEqual( Buffer.from(expected), Buffer.from(signature)); } const ok = isValidAttioWebhook( rawBody, req.headers['attio-signature'], process.env.ATTIO_WEBHOOK_SECRET); ``` **Python** ``` import hashlib, hmac def is_valid_attio_webhook(raw_body: bytes, signature: str, secret: str) -> bool: expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, signature) ``` ## **Frequently asked questions**
**Where is the Attio webhook secret?** On the webhook's page in Attio developer settings, and in the API response when the webhook is created. Each webhook has its own secret.
**What encoding does the signature use?** Lowercase hex of the HMAC-SHA256 digest — no prefix, no base64. If your computed value looks right but longer or shorter, check you are hex-encoding, not base64-encoding.
**Does Attio batch events?** Yes — a single delivery carries an `events` array, each entry with its own `event_type` and record identifiers, under one `webhook_id`. Verify the delivery once, then iterate the array.
## **Verify other providers** [![Stripe logo](https://webhookrelay.com/verify-attio-webhook-signature/images/landing/logos/stripe.svg)Stripe](https://webhookrelay.com/verify-attio-webhook-signature/verify-stripe-webhook-signature/) [![GitHub logo](https://webhookrelay.com/verify-attio-webhook-signature/images/landing/logos/github.svg) GitHub](https://webhookrelay.com/verify-attio-webhook-signature/verify-github-webhook-signature/) [![Shopify logo](https://webhookrelay.com/verify-attio-webhook-signature/images/landing/logos/shopify.svg) Shopify](https://webhookrelay.com/verify-attio-webhook-signature/verify-shopify-webhook-signature/) [![Slack logo](https://webhookrelay.com/verify-attio-webhook-signature/images/landing/logos/slack.svg) Slack](https://webhookrelay.com/verify-attio-webhook-signature/verify-slack-webhook-signature/) [![Twilio logo](https://webhookrelay.com/verify-attio-webhook-signature/images/landing/logos/twilio.svg) Twilio](https://webhookrelay.com/verify-attio-webhook-signature/verify-twilio-webhook-signature/) [**S** Standard Webhooks](https://webhookrelay.com/verify-attio-webhook-signature/verify-standard-webhooks-signature/) [![Square logo](https://webhookrelay.com/verify-attio-webhook-signature/images/landing/logos/square.svg) Square](https://webhookrelay.com/verify-attio-webhook-signature/verify-square-webhook-signature/) [**L** LINE](https://webhookrelay.com/verify-attio-webhook-signature/verify-line-webhook-signature/) [**A** Airwallex](https://webhookrelay.com/verify-attio-webhook-signature/verify-airwallex-webhook-signature/) [**M** MyFatoorah](https://webhookrelay.com/verify-attio-webhook-signature/verify-myfatoorah-webhook-signature/) [![Zendesk logo](https://webhookrelay.com/verify-attio-webhook-signature/images/landing/logos/zendesk.svg) Zendesk](https://webhookrelay.com/verify-attio-webhook-signature/verify-zendesk-webhook-signature/) [![DocuSign logo](https://webhookrelay.com/verify-attio-webhook-signature/images/landing/logos/docusign.svg) DocuSign](https://webhookrelay.com/verify-attio-webhook-signature/verify-docusign-webhook-signature/) [**K** Klarna](https://webhookrelay.com/verify-attio-webhook-signature/verify-klarna-webhook-signature/) [HMAC Generator](https://webhookrelay.com/verify-attio-webhook-signature/hmac-verification/) [CloudEvents Validator](https://webhookrelay.com/verify-attio-webhook-signature/cloudevents-validator/) [Webhook Tester (Bin)](https://webhookrelay.com/verify-attio-webhook-signature/webhook-bin/) Receiving Attio webhooks on a server behind a firewall or on localhost? Webhook Relay can [forward them to your internal service](https://webhookrelay.com/verify-attio-webhook-signature/webhooks/) and even [verify or transform them](https://webhookrelay.com/verify-attio-webhook-signature/features/transform-webhooks/) before delivery. --- --- title: GCP BigQuery | WebhookRelay meta: "og: title": "GCP BigQuery" description: Learn to insert and stream data into BigQuery from webhooks url: https://webhookrelay.com/docs/tutorials/warehouse/bigquery.md file: /docs/tutorials/warehouse/bigquery.md --- ![Stripes](https://webhookrelay.com/docs/tutorials/warehouse/bigquery/images/stripes.svg) Documentation **Fundamentals** # **GCP BigQuery** Learn to insert and stream data into BigQuery from webhooks ![Webhook Relay to BigQuery](https://webhookrelay.com/docs/tutorials/warehouse/bigquery/images/tutorials/functions/whr-to-bigquery.png) Prerequisites: - [Google Cloud Platform account](https://cloud.google.com/) (free trial available) - Google Cloud project with BigQuery enabled (there's a generous free tier available for BigQuery) - Dataset and table in BigQuery - [https://cloud.google.com/bigquery/docs/tables](https://cloud.google.com/bigquery/docs/tables) Webhook Relay provides a helper package **bigquery** that can stream writes into [Google Cloud BigQuery](https://cloud.google.com/bigquery/). To start ingesting data from webhooks straight into your BigQuery table, create a new Function and just import 'bigquery' package: ``` -- Import BigQuery helper package local bigquery = require('bigquery') ``` A new tab should appear that will ask you to set up credentials: ![Configure GCP credentials](https://webhookrelay.com/docs/tutorials/warehouse/bigquery/images/tutorials/functions/configure-gcp.png) Go to that tab and it will ask you to: 1. Create new [service accounts](https://cloud.google.com/iam/docs/service-accounts) with **BigQuery Editor** permissions 2. Download the JSON file. Once you have the JSON file 3. Copy & paste contents into the form and click save. ## [Streaming data to GCP BigQuery](#streaming-data-to-gcp-bigquery) ``` -- Import BigQuery helper package local bigquery = require('bigquery') local json = require("json") -- Parsing payload local rowData, err = json.decode(r.RequestBody) if err then error(err) end -- Initializing BigQuery client err = bigquery.initialize('your-project-id', 'dataset-id', 'table-id') if err then error(err) end -- Receiving payload: -- { -- "hub_user_id": "user-id-here", -- "category": "signup", -- "action": "click", -- "label": "github auth" -- } -- Insert row: err = bigquery.insert(rowData) if err then error(err) end ``` ## [BigQuery package API reference](#bigquery-package-api-reference) At the moment there's a single client method that bigquery package exposes: | Method name | Parameter Type | Description | | --- | --- | --- | | insert(rowData) | Table | A table keyvalue that represents a row data. | ## [Limitations](#limitations) Currently our package doesn't support nested objects. That means that a table that a structure that represents JSON such as: ``` { "hub_user_id": "user-id-here", "category": "signup", "action": "click", "label": "github auth", "nested_data": { "location": "GB", "date": "2020-05-10" } } ``` will not be successfully inserted. Therefore, flatten the structure in the function before inserting it. ## [Troubleshooting](#troubleshooting) Few things to note: - Ensure that project ID, dataset ID and table ID are there. - BigQuery table schema is defined by the user. You don't have to write all the fields (most of the can be nullable) but if you try to write a field that doesn't exist, BigQuery will refuse to write. Did this page help you? --- --- title: Free GitHub Webhook Signature Verifier | WebhookRelay meta: "og: title": "Free GitHub Webhook Signature Verifier" description: Free GitHub webhook signature verifier. Validate the X-Hub-Signature-256 header against your webhook secret. No signup, runs in your browser. url: https://webhookrelay.com/verify-github-webhook-signature.md file: /verify-github-webhook-signature.md --- # **Verify GitHub Webhook Signatures** GitHub signs webhook deliveries with HMAC-SHA256 over the raw request body and sends the result in the `X-Hub-Signature-256` header (formatted `sha256=…`). Paste the raw body, the secret you configured on the webhook, and the header value. **Raw request body (payload)** **Webhook secret** **Computed signature** **Signature to verify **(X-Hub-Signature-256) **Paste the X-Hub-Signature-256 value above to compare** Everything runs in your browser — the payload and secret never leave this page. Want to verify a different provider? See the [webhook signature verifier hub](https://webhookrelay.com/verify-github-webhook-signature/verify-webhook-signature/) or the generic [HMAC generator](https://webhookrelay.com/verify-github-webhook-signature/hmac-verification/). ## **How GitHub signs webhooks** 1. Take the **raw** request body exactly as delivered. 2. Compute HMAC-SHA256 using your webhook secret as the key, hex-encoded. 3. Prefix it with `sha256=` and compare to `X-Hub-Signature-256` with a constant-time check. **References & official docs:** - [GitHub — Validating webhook deliveries](https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries) - [GitHub — Securing your webhooks](https://docs.github.com/en/webhooks/using-webhooks/securing-your-webhooks) ## **Verify GitHub signatures in code** **Node.js** ``` const crypto = require('crypto'); const expected = 'sha256=' + crypto .createHmac('sha256', process.env.WEBHOOK_SECRET) .update(rawBody) // raw bytes of the request body .digest('hex'); const received = req.headers['x-hub-signature-256']; const valid = crypto.timingSafeEqual( Buffer.from(expected), Buffer.from(received)); ``` **Python** ``` import hmac, hashlib expected = 'sha256=' + hmac.new( secret.encode(), raw_body, hashlib.sha256).hexdigest() received = request.headers['X-Hub-Signature-256'] valid = hmac.compare_digest(expected, received) ``` ## **Frequently asked questions**
**X-Hub-Signature vs X-Hub-Signature-256?** GitHub sends both. `X-Hub-Signature` is the legacy SHA-1 version; always verify `X-Hub-Signature-256` (SHA-256) instead.
**Where do I set the secret?** In the repository or organization webhook settings, in the "Secret" field. GitHub then signs every delivery with it; an empty secret means no signature is sent.
## **Verify other providers** [![Stripe logo](https://webhookrelay.com/verify-github-webhook-signature/images/landing/logos/stripe.svg)Stripe](https://webhookrelay.com/verify-github-webhook-signature/verify-stripe-webhook-signature/) [![Shopify logo](https://webhookrelay.com/verify-github-webhook-signature/images/landing/logos/shopify.svg) Shopify](https://webhookrelay.com/verify-github-webhook-signature/verify-shopify-webhook-signature/) [![Slack logo](https://webhookrelay.com/verify-github-webhook-signature/images/landing/logos/slack.svg) Slack](https://webhookrelay.com/verify-github-webhook-signature/verify-slack-webhook-signature/) [![Twilio logo](https://webhookrelay.com/verify-github-webhook-signature/images/landing/logos/twilio.svg) Twilio](https://webhookrelay.com/verify-github-webhook-signature/verify-twilio-webhook-signature/) [**S** Standard Webhooks](https://webhookrelay.com/verify-github-webhook-signature/verify-standard-webhooks-signature/) [![Square logo](https://webhookrelay.com/verify-github-webhook-signature/images/landing/logos/square.svg) Square](https://webhookrelay.com/verify-github-webhook-signature/verify-square-webhook-signature/) [**L** LINE](https://webhookrelay.com/verify-github-webhook-signature/verify-line-webhook-signature/) [**A** Airwallex](https://webhookrelay.com/verify-github-webhook-signature/verify-airwallex-webhook-signature/) [**M** MyFatoorah](https://webhookrelay.com/verify-github-webhook-signature/verify-myfatoorah-webhook-signature/) [![Zendesk logo](https://webhookrelay.com/verify-github-webhook-signature/images/landing/logos/zendesk.svg) Zendesk](https://webhookrelay.com/verify-github-webhook-signature/verify-zendesk-webhook-signature/) [![DocuSign logo](https://webhookrelay.com/verify-github-webhook-signature/images/landing/logos/docusign.svg) DocuSign](https://webhookrelay.com/verify-github-webhook-signature/verify-docusign-webhook-signature/) [**K** Klarna](https://webhookrelay.com/verify-github-webhook-signature/verify-klarna-webhook-signature/) [**A** Attio](https://webhookrelay.com/verify-github-webhook-signature/verify-attio-webhook-signature/) [HMAC Generator](https://webhookrelay.com/verify-github-webhook-signature/hmac-verification/) [CloudEvents Validator](https://webhookrelay.com/verify-github-webhook-signature/cloudevents-validator/) [Webhook Tester (Bin)](https://webhookrelay.com/verify-github-webhook-signature/webhook-bin/) Receiving GitHub webhooks on a server behind a firewall or on localhost? Webhook Relay can [forward them to your internal service](https://webhookrelay.com/verify-github-webhook-signature/webhooks/) and even [verify or transform them](https://webhookrelay.com/verify-github-webhook-signature/features/transform-webhooks/) before delivery. --- --- title: Free LINE Webhook Signature Verifier | WebhookRelay meta: "og: title": "Free LINE Webhook Signature Verifier" description: Free LINE webhook signature verifier. Validate the x-line-signature header against your channel secret. No signup, runs in your browser. url: https://webhookrelay.com/verify-line-webhook-signature.md file: /verify-line-webhook-signature.md --- # **Verify LINE Webhook Signatures** The LINE Messaging API signs each webhook with HMAC-SHA256 over the raw request body and sends the **base64** result in the `x-line-signature` header. The key is your channel secret from the LINE Developers Console. Paste the raw body, the channel secret and the header value. **Raw request body (payload)** **Channel secret** **Computed signature** **Signature to verify **(x-line-signature) **Paste the x-line-signature value above to compare** Everything runs in your browser — the payload and secret never leave this page. Want to verify a different provider? See the [webhook signature verifier hub](https://webhookrelay.com/verify-line-webhook-signature/verify-webhook-signature/) or the generic [HMAC generator](https://webhookrelay.com/verify-line-webhook-signature/hmac-verification/). ## **How LINE signs webhooks** 1. Take the **raw** request body exactly as received — do not parse or re-serialize it, as even changing escape characters breaks the signature. 2. Compute HMAC-SHA256 using your channel secret as the key. 3. Base64-encode the digest and constant-time compare it to `x-line-signature`. **References & official docs:** - [LINE — Verify webhook signature](https://developers.line.biz/en/docs/messaging-api/verify-webhook-signature/) - [LINE — Messaging API reference: signature validation](https://developers.line.biz/en/reference/messaging-api/) ## **Verify LINE signatures in code** **Node.js** ``` const crypto = require('crypto'); const expected = crypto .createHmac('sha256', process.env.LINE_CHANNEL_SECRET) .update(rawBody) // raw request body, unmodified .digest('base64'); const received = req.headers['x-line-signature']; const valid = crypto.timingSafeEqual( Buffer.from(expected), Buffer.from(received)); ``` **Python** ``` import hmac, hashlib, base64 expected = base64.b64encode(hmac.new( channel_secret.encode(), raw_body, hashlib.sha256).digest()).decode() received = request.headers['X-Line-Signature'] valid = hmac.compare_digest(expected, received) ``` ## **Frequently asked questions**
**Where is the LINE channel secret?** In the LINE Developers Console on your channel's "Basic settings" tab. It is used as a raw UTF-8 string. Re-issuing the channel secret immediately invalidates the previous one.
**Why does LINE signature verification fail?** Almost always because the body was parsed and re-serialized. LINE signs the exact bytes — including escape characters and UTF-8 line breaks — so verify against the unmodified raw body before any JSON parsing.
## **Verify other providers** [![Stripe logo](https://webhookrelay.com/verify-line-webhook-signature/images/landing/logos/stripe.svg)Stripe](https://webhookrelay.com/verify-line-webhook-signature/verify-stripe-webhook-signature/) [![GitHub logo](https://webhookrelay.com/verify-line-webhook-signature/images/landing/logos/github.svg) GitHub](https://webhookrelay.com/verify-line-webhook-signature/verify-github-webhook-signature/) [![Shopify logo](https://webhookrelay.com/verify-line-webhook-signature/images/landing/logos/shopify.svg) Shopify](https://webhookrelay.com/verify-line-webhook-signature/verify-shopify-webhook-signature/) [![Slack logo](https://webhookrelay.com/verify-line-webhook-signature/images/landing/logos/slack.svg) Slack](https://webhookrelay.com/verify-line-webhook-signature/verify-slack-webhook-signature/) [![Twilio logo](https://webhookrelay.com/verify-line-webhook-signature/images/landing/logos/twilio.svg) Twilio](https://webhookrelay.com/verify-line-webhook-signature/verify-twilio-webhook-signature/) [**S** Standard Webhooks](https://webhookrelay.com/verify-line-webhook-signature/verify-standard-webhooks-signature/) [![Square logo](https://webhookrelay.com/verify-line-webhook-signature/images/landing/logos/square.svg) Square](https://webhookrelay.com/verify-line-webhook-signature/verify-square-webhook-signature/) [**A** Airwallex](https://webhookrelay.com/verify-line-webhook-signature/verify-airwallex-webhook-signature/) [**M** MyFatoorah](https://webhookrelay.com/verify-line-webhook-signature/verify-myfatoorah-webhook-signature/) [![Zendesk logo](https://webhookrelay.com/verify-line-webhook-signature/images/landing/logos/zendesk.svg) Zendesk](https://webhookrelay.com/verify-line-webhook-signature/verify-zendesk-webhook-signature/) [![DocuSign logo](https://webhookrelay.com/verify-line-webhook-signature/images/landing/logos/docusign.svg) DocuSign](https://webhookrelay.com/verify-line-webhook-signature/verify-docusign-webhook-signature/) [**K** Klarna](https://webhookrelay.com/verify-line-webhook-signature/verify-klarna-webhook-signature/) [**A** Attio](https://webhookrelay.com/verify-line-webhook-signature/verify-attio-webhook-signature/) [HMAC Generator](https://webhookrelay.com/verify-line-webhook-signature/hmac-verification/) [CloudEvents Validator](https://webhookrelay.com/verify-line-webhook-signature/cloudevents-validator/) [Webhook Tester (Bin)](https://webhookrelay.com/verify-line-webhook-signature/webhook-bin/) Receiving LINE webhooks on a server behind a firewall or on localhost? Webhook Relay can [forward them to your internal service](https://webhookrelay.com/verify-line-webhook-signature/webhooks/) and even [verify or transform them](https://webhookrelay.com/verify-line-webhook-signature/features/transform-webhooks/) before delivery. --- --- title: Enrich webhooks from APIs | WebhookRelay meta: "og: title": "Enrich webhooks from APIs" description: Call 3rd party API and transform your webhook before sending it to the final destination url: https://webhookrelay.com/docs/tutorials/transform/enrich-webhooks.md file: /docs/tutorials/transform/enrich-webhooks.md --- ![Stripes](https://webhookrelay.com/docs/tutorials/transform/enrich-webhooks/images/stripes.svg) Documentation **Fundamentals** # **Enrich webhooks from APIs** Call 3rd party API and transform your webhook before sending it to the final destination While integrating several systems with each other, quite often you need not just transform the webhook but fetch additional data from a 3rd party API or your own service. To do this, use **http** package which can be imported into your functions. ![running the function](https://webhookrelay.com/docs/tutorials/transform/enrich-webhooks/images/tutorials/functions/function-http-call.png) ## [Making HTTP call](#making-http-call) In this example we will fetch some data from HTTP API and merge it together with the incoming webhook to get a new message: ``` local http = require("http") local json = require("json") -- API returns a JSON containing our pets and prices: -- { -- "cat": { -- "size": "small", -- "price": 50 -- }, -- "dog": { -- "size": "medium", -- "price": 120 -- }, -- "cow": { -- "size": "large", -- "price": 600 -- } -- } response, error_message = http.request("GET", "https://gist.githubusercontent.com/rusenask/c1b5840c62a70ea11fdedd9a6aabbd03/raw/8a0177791d94c22fdb9345243392c62ddb10a10f/pets.json") if error_message then error(error_message) end -- Parsing response body from the API local api_response, err = json.decode(response.body) if err then error(err) end -- Parsing webhook body -- { -- "pet": "cat", -- "quantity": 2 -- } local request_body, err = json.decode(r.RequestBody) if err then error(err) end local message = "Purchased pet: " .. request_body["pet"] .. " | quantity: " .. request_body["quantity"] .. " | total:" .. request_body["quantity"] * api_response[request_body["pet"]]["price"] -- Preparing new payload local new_payload = { action= "purchased", message= message} local encoded_payload, err = json.encode(new_payload) if err then error(err) end -- Set request header to application/json r:SetRequestHeader("Content-Type", "application/json") -- Set request method to PUT r:SetRequestMethod("PUT") -- Set modified request body r:SetRequestBody(encoded_payload) ``` ## [Running the function](#running-the-function) Now, if you send a request such as: ``` { "pet": "cat", "quantity": 3 } ``` To Input our Output in Webhook Relay (that has this Function attached), response is: ``` { "action": "purchased", "message": "Purchased pet: cat | quantity: 3 | total:150" } ``` Did this page help you? --- --- title: Free MyFatoorah Webhook Signature Verifier | WebhookRelay meta: "og: title": "Free MyFatoorah Webhook Signature Verifier" description: Free MyFatoorah webhook signature verifier. Validate the MyFatoorah-Signature header against the canonical Key=Value string. No signup, runs in your browser. url: https://webhookrelay.com/verify-myfatoorah-webhook-signature.md file: /verify-myfatoorah-webhook-signature.md --- # **Verify MyFatoorah Webhook Signatures** MyFatoorah v2 webhooks don't sign the raw body — they build a canonical `Key=Value,…` string from specific `Data` fields in a fixed per-event order, HMAC-SHA256 it with your webhook secret key, and send the **base64** result in the `MyFatoorah-Signature` header. Construct that string (see the steps below), then paste it with your secret and the header value. **Signed string (ordered Key=Value pairs)** MyFatoorah v2 signs a comma-joined `Key=Value` string built from specific `Data` fields in a fixed order per event — **not** the raw body. Build that string (see below) and paste it here. **Webhook secret key** **Computed signature** **Signature to verify **(MyFatoorah-Signature) **Paste the MyFatoorah-Signature value above to compare** Everything runs in your browser — the payload and secret never leave this page. Want to verify a different provider? See the [webhook signature verifier hub](https://webhookrelay.com/verify-myfatoorah-webhook-signature/verify-webhook-signature/) or the generic [HMAC generator](https://webhookrelay.com/verify-myfatoorah-webhook-signature/hmac-verification/). ## **How MyFatoorah signs webhooks** 1. Take the fields from the webhook's `Data` object in the **exact order documented for that event**. For a payment status change: `Invoice.Id`, `Invoice.Status`, `Transaction.Status`, `Transaction.PaymentId`, `Invoice.ExternalIdentifier`. 2. Join them as `Key=Value` pairs separated by commas, replacing any null value with an empty string. This is the signed string (the order is fixed per event — it is not alphabetical). 3. Compute HMAC-SHA256 of that string using your webhook secret key (UTF-8) as the key. 4. Base64-encode the digest and constant-time compare it to the `MyFatoorah-Signature` header. **References & official docs:** - [MyFatoorah — Webhook signature](https://docs.myfatoorah.com/docs/webhook-signature) - [MyFatoorah — Webhook v2 overview](https://docs.myfatoorah.com/docs/webhook-v2) - [MyFatoorah — Payment status data model (field order)](https://docs.myfatoorah.com/docs/webhook-v2-payment-status-data-model) ## **Verify MyFatoorah signatures in code** **Node.js** ``` const crypto = require('crypto'); // Fixed field order per MyFatoorah v2 event (dot-paths into payload.Data). const FIELDS = { PAYMENT_STATUS_CHANGED: ['Invoice.Id','Invoice.Status','Transaction.Status','Transaction.PaymentId','Invoice.ExternalIdentifier'], REFUND_STATUS_CHANGED: ['Refund.Id','Refund.Status','Amount.ValueInBaseCurrency','ReferencedInvoice.Id'], }; const get = (o, path) => { const v = path.split('.').reduce((x, k) => (x == null ? undefined : x[k]), o); return v == null ? '' : String(v); // null -> empty string }; const event = JSON.parse(rawBody); const signed = FIELDS[event.Event.Name] .map(f => \`${f}=${get(event.Data, f)}\`).join(','); const expected = crypto .createHmac('sha256', process.env.MYFATOORAH_WEBHOOK_SECRET) .update(signed).digest('base64'); const valid = crypto.timingSafeEqual( Buffer.from(expected), Buffer.from(req.headers['myfatoorah-signature'])); ``` **Python** ``` import hmac, hashlib, base64, json FIELDS = { "PAYMENT_STATUS_CHANGED": ["Invoice.Id","Invoice.Status","Transaction.Status","Transaction.PaymentId","Invoice.ExternalIdentifier"], "REFUND_STATUS_CHANGED": ["Refund.Id","Refund.Status","Amount.ValueInBaseCurrency","ReferencedInvoice.Id"], } def get(data, path): cur = data for k in path.split("."): cur = cur.get(k) if isinstance(cur, dict) else None return "" if cur is None else str(cur) # null -> empty string event = json.loads(raw_body) signed = ",".join(f"{f}={get(event['Data'], f)}" for f in FIELDS[event["Event"]["Name"]]) expected = base64.b64encode(hmac.new( secret.encode(), signed.encode(), hashlib.sha256).digest()).decode() valid = hmac.compare_digest(expected, request.headers["MyFatoorah-Signature"]) ``` ## **Frequently asked questions**
**What does MyFatoorah actually sign?** Not the raw JSON body. It signs a comma-separated `Key=Value` string built from a fixed set of `Data` fields, in the exact order documented for that event type, with any null value replaced by an empty string.
**Which fields go into the signed string?** It depends on the event. For `PAYMENT_STATUS_CHANGED` it is `Invoice.Id`, `Invoice.Status`, `Transaction.Status`, `Transaction.PaymentId`, `Invoice.ExternalIdentifier`. Each event's data-model page lists its own order — follow it exactly, it is not alphabetical.
**Where is the webhook secret key?** In the MyFatoorah portal under Integration Settings → Webhook Settings, where you also choose v1 or v2. v2 requires the secret key. It is used as a raw UTF-8 string, not decoded.
## **Verify other providers** [![Stripe logo](https://webhookrelay.com/verify-myfatoorah-webhook-signature/images/landing/logos/stripe.svg)Stripe](https://webhookrelay.com/verify-myfatoorah-webhook-signature/verify-stripe-webhook-signature/) [![GitHub logo](https://webhookrelay.com/verify-myfatoorah-webhook-signature/images/landing/logos/github.svg) GitHub](https://webhookrelay.com/verify-myfatoorah-webhook-signature/verify-github-webhook-signature/) [![Shopify logo](https://webhookrelay.com/verify-myfatoorah-webhook-signature/images/landing/logos/shopify.svg) Shopify](https://webhookrelay.com/verify-myfatoorah-webhook-signature/verify-shopify-webhook-signature/) [![Slack logo](https://webhookrelay.com/verify-myfatoorah-webhook-signature/images/landing/logos/slack.svg) Slack](https://webhookrelay.com/verify-myfatoorah-webhook-signature/verify-slack-webhook-signature/) [![Twilio logo](https://webhookrelay.com/verify-myfatoorah-webhook-signature/images/landing/logos/twilio.svg) Twilio](https://webhookrelay.com/verify-myfatoorah-webhook-signature/verify-twilio-webhook-signature/) [**S** Standard Webhooks](https://webhookrelay.com/verify-myfatoorah-webhook-signature/verify-standard-webhooks-signature/) [![Square logo](https://webhookrelay.com/verify-myfatoorah-webhook-signature/images/landing/logos/square.svg) Square](https://webhookrelay.com/verify-myfatoorah-webhook-signature/verify-square-webhook-signature/) [**L** LINE](https://webhookrelay.com/verify-myfatoorah-webhook-signature/verify-line-webhook-signature/) [**A** Airwallex](https://webhookrelay.com/verify-myfatoorah-webhook-signature/verify-airwallex-webhook-signature/) [![Zendesk logo](https://webhookrelay.com/verify-myfatoorah-webhook-signature/images/landing/logos/zendesk.svg) Zendesk](https://webhookrelay.com/verify-myfatoorah-webhook-signature/verify-zendesk-webhook-signature/) [![DocuSign logo](https://webhookrelay.com/verify-myfatoorah-webhook-signature/images/landing/logos/docusign.svg) DocuSign](https://webhookrelay.com/verify-myfatoorah-webhook-signature/verify-docusign-webhook-signature/) [**K** Klarna](https://webhookrelay.com/verify-myfatoorah-webhook-signature/verify-klarna-webhook-signature/) [**A** Attio](https://webhookrelay.com/verify-myfatoorah-webhook-signature/verify-attio-webhook-signature/) [HMAC Generator](https://webhookrelay.com/verify-myfatoorah-webhook-signature/hmac-verification/) [CloudEvents Validator](https://webhookrelay.com/verify-myfatoorah-webhook-signature/cloudevents-validator/) [Webhook Tester (Bin)](https://webhookrelay.com/verify-myfatoorah-webhook-signature/webhook-bin/) Receiving MyFatoorah webhooks on a server behind a firewall or on localhost? Webhook Relay can [forward them to your internal service](https://webhookrelay.com/verify-myfatoorah-webhook-signature/webhooks/) and even [verify or transform them](https://webhookrelay.com/verify-myfatoorah-webhook-signature/features/transform-webhooks/) before delivery. --- --- title: Free Shopify Webhook Signature Verifier | WebhookRelay meta: "og: title": "Free Shopify Webhook Signature Verifier" description: Free Shopify webhook signature verifier. Validate the X-Shopify-Hmac-Sha256 header against your app's API secret. No signup, runs in your browser. url: https://webhookrelay.com/verify-shopify-webhook-signature.md file: /verify-shopify-webhook-signature.md --- # **Verify Shopify Webhook Signatures** Shopify signs webhooks with HMAC-SHA256 over the raw request body and sends a **base64**-encoded digest in the `X-Shopify-Hmac-Sha256` header. The key is your app's API secret key (client secret). Paste the raw body, the secret and the header value. **Raw request body (payload)** **API secret key** **Computed signature** **Signature to verify **(X-Shopify-Hmac-Sha256) **Paste the X-Shopify-Hmac-Sha256 value above to compare** Everything runs in your browser — the payload and secret never leave this page. Want to verify a different provider? See the [webhook signature verifier hub](https://webhookrelay.com/verify-shopify-webhook-signature/verify-webhook-signature/) or the generic [HMAC generator](https://webhookrelay.com/verify-shopify-webhook-signature/hmac-verification/). ## **How Shopify signs webhooks** 1. Take the **raw** request body exactly as delivered. 2. Compute HMAC-SHA256 using your app's API secret key as the key. 3. Base64-encode the digest and compare it to `X-Shopify-Hmac-Sha256` with a constant-time check. **References & official docs:** - [Shopify — Verify a webhook (HTTPS delivery)](https://shopify.dev/docs/apps/build/webhooks/subscribe/https) - [Shopify — Webhooks overview](https://shopify.dev/docs/apps/build/webhooks) ## **Verify Shopify signatures in code** **Node.js** ``` const crypto = require('crypto'); const digest = crypto .createHmac('sha256', process.env.SHOPIFY_API_SECRET) .update(rawBody, 'utf8') // raw request body .digest('base64'); const received = req.headers['x-shopify-hmac-sha256']; const valid = crypto.timingSafeEqual( Buffer.from(digest), Buffer.from(received)); ``` **Python** ``` import hmac, hashlib, base64 digest = base64.b64encode(hmac.new( api_secret.encode(), raw_body, hashlib.sha256).digest()).decode() received = request.headers['X-Shopify-Hmac-Sha256'] valid = hmac.compare_digest(digest, received) ``` ## **Frequently asked questions**
**Which secret signs Shopify webhooks?** Your app's API secret key (client secret) for app webhooks. Webhooks created in the Shopify admin are signed with a separate secret shown in Settings → Notifications.
**Why must I use the raw body?** Shopify computes the HMAC over the exact bytes sent. Parsing and re-serializing the JSON changes whitespace and key order, which breaks the digest.
## **Verify other providers** [![Stripe logo](https://webhookrelay.com/verify-shopify-webhook-signature/images/landing/logos/stripe.svg)Stripe](https://webhookrelay.com/verify-shopify-webhook-signature/verify-stripe-webhook-signature/) [![GitHub logo](https://webhookrelay.com/verify-shopify-webhook-signature/images/landing/logos/github.svg) GitHub](https://webhookrelay.com/verify-shopify-webhook-signature/verify-github-webhook-signature/) [![Slack logo](https://webhookrelay.com/verify-shopify-webhook-signature/images/landing/logos/slack.svg) Slack](https://webhookrelay.com/verify-shopify-webhook-signature/verify-slack-webhook-signature/) [![Twilio logo](https://webhookrelay.com/verify-shopify-webhook-signature/images/landing/logos/twilio.svg) Twilio](https://webhookrelay.com/verify-shopify-webhook-signature/verify-twilio-webhook-signature/) [**S** Standard Webhooks](https://webhookrelay.com/verify-shopify-webhook-signature/verify-standard-webhooks-signature/) [![Square logo](https://webhookrelay.com/verify-shopify-webhook-signature/images/landing/logos/square.svg) Square](https://webhookrelay.com/verify-shopify-webhook-signature/verify-square-webhook-signature/) [**L** LINE](https://webhookrelay.com/verify-shopify-webhook-signature/verify-line-webhook-signature/) [**A** Airwallex](https://webhookrelay.com/verify-shopify-webhook-signature/verify-airwallex-webhook-signature/) [**M** MyFatoorah](https://webhookrelay.com/verify-shopify-webhook-signature/verify-myfatoorah-webhook-signature/) [![Zendesk logo](https://webhookrelay.com/verify-shopify-webhook-signature/images/landing/logos/zendesk.svg) Zendesk](https://webhookrelay.com/verify-shopify-webhook-signature/verify-zendesk-webhook-signature/) [![DocuSign logo](https://webhookrelay.com/verify-shopify-webhook-signature/images/landing/logos/docusign.svg) DocuSign](https://webhookrelay.com/verify-shopify-webhook-signature/verify-docusign-webhook-signature/) [**K** Klarna](https://webhookrelay.com/verify-shopify-webhook-signature/verify-klarna-webhook-signature/) [**A** Attio](https://webhookrelay.com/verify-shopify-webhook-signature/verify-attio-webhook-signature/) [HMAC Generator](https://webhookrelay.com/verify-shopify-webhook-signature/hmac-verification/) [CloudEvents Validator](https://webhookrelay.com/verify-shopify-webhook-signature/cloudevents-validator/) [Webhook Tester (Bin)](https://webhookrelay.com/verify-shopify-webhook-signature/webhook-bin/) Receiving Shopify webhooks on a server behind a firewall or on localhost? Webhook Relay can [forward them to your internal service](https://webhookrelay.com/verify-shopify-webhook-signature/webhooks/) and even [verify or transform them](https://webhookrelay.com/verify-shopify-webhook-signature/features/transform-webhooks/) before delivery. --- --- title: DockerHub webhook to Slack notification | WebhookRelay meta: "og: title": "DockerHub webhook to Slack notification" description: Use Lua function to convert DockerHub webhook request to Slack channel notification url: https://webhookrelay.com/docs/tutorials/transform/docker-to-slack.md file: /docs/tutorials/transform/docker-to-slack.md --- ![Stripes](https://webhookrelay.com/docs/tutorials/transform/docker-to-slack/images/stripes.svg) Documentation **Fundamentals** # **DockerHub webhook to Slack notification** Use Lua function to convert DockerHub webhook request to Slack channel notification Many Docker registries provide a way to notify team in chat channels when new images are pushed (if you are waiting for a build complete). Let's add this capability to the official DockerHub registry! Prerequisites: - [Webhook Relay account](https://my.webhookrelay.com/) - [DockerHub account](https://hub.docker.com/) - Docker ### [Create a bucket and configure DockerHub notification](#create-a-bucket-and-configure-dockerhub-notification) 1. Create a bucket here [https://my.webhookrelay.com/buckets](https://my.webhookrelay.com/buckets) 2. Once you have it, in the inputs section you will find your public input endpoint, copy it: ![Input endpoint URL](https://webhookrelay.com/docs/tutorials/transform/docker-to-slack/images/docs/webhooks/functions/input-endpoint.png) 1. Add a new DockerHub webhook setting pointing at our public input endpoint ([DockerHub docs](https://docs.docker.com/docker-hub/webhooks/)): ![Dockerhub webhook config](https://webhookrelay.com/docs/tutorials/transform/docker-to-slack/images/docs/webhooks/functions/dockerhub-config.png) ### [Get a sample of DockerHub webhook](#get-a-sample-of-dockerhub-webhook) Push a new Docker image: ``` $ docker push karolisr/demo-webhook:latest The push refers to repository [docker.io/karolisr/demo-webhook] 48bd38e03c42: Mounted from karolisr/webhook-demo fd9f9fbd5947: Mounted from karolisr/webhook-demo 5216338b40a7: Mounted from karolisr/webhook-demo latest: digest: sha256:703f2bab2ce8df0c5ec4e45e26718954b09bf4a625ab831c6556fd27d60f1325 size: 949 ``` We should be able to see a new incoming webhook. It looks like this: ``` { "push_data": { "pushed_at": 1582839308, "images": [], "tag": "latest", "pusher": "karolisr" }, "callback_url": "https://registry.hub.docker.com/u/karolisr/demo-webhook/hook/242ii4ddc2jji4a0cc44fbcdbcdecj1ab/", "repository": { "status": "Active", "description": "", "is_trusted": false, "full_description": "", "repo_url": "https://hub.docker.com/r/karolisr/demo-webhook", "owner": "karolisr", "is_official": false, "is_private": true, "name": "demo-webhook", "namespace": "karolisr", "star_count": 0, "comment_count": 0, "date_created": 1524557040, "repo_name": "karolisr/demo-webhook" } } ``` ### [Create a Function to transform the webhook](#create-a-function-to-transform-the-webhook) Go to the [Functions page](https://my.webhookrelay.com/functions) and click on a "Create Function" button. Enter a name, for example "dockerhub-to-slack" and click "Submit". You can now copy/paste webhook payload into the "request body" area for later tests. In the code editor let's add a function to get repository name and prepare a Slack webhook payload (currently functions have to be written in Lua but more examples for WebAssembly will be added soon): ``` local json = require("json") local body, err = json.decode(r.RequestBody) if err then error(err) end local message = "New image pushed at: " .. body["repository"]["repo_name"] .. ":" .. body["push_data"]["tag"] -- Preparing Slack payload local slack = { response_type= "in_channel", text= message} local result, err = json.encode(slack) -- Set request header to application/json r:SetRequestHeader("Content-Type", "application/json") -- Set request method to PUT r:SetRequestMethod("POST") -- Set modified request body r:SetRequestBody(result) ``` Click "Save" and then try testing it with the "Send" button: ![Function invoke example](https://webhookrelay.com/docs/tutorials/transform/docker-to-slack/images/docs/webhooks/functions/function-invoke-example.png) ### [Connect everything together](#connect-everything-together) 1. Navigate to [https://api.slack.com/messaging/webhooks](https://api.slack.com/messaging/webhooks) and click "Create your Slack app". Select your workspace, enter a name that you will remember. 2. Create a new incoming webhook configuration, copy "Webhook URL" (it starts with `https://hooks.slack.com/services/T3...`), we will need to supply it to Webhook Relay. 3. Open your bucket details (via [https://my.webhookrelay.com/buckets](https://my.webhookrelay.com/buckets)) 4. Open "OUTPUT DESTINATIONS" tab and create a new output called "Slack" with the Slack URL from step 2: ![Create destination](https://webhookrelay.com/docs/tutorials/transform/docker-to-slack/images/docs/webhooks/functions/create-destination.png) 1. Once created, click on the "code" symbol and from the dropdown select `dockerhub_to_slack` function: ![Select function](https://webhookrelay.com/docs/tutorials/transform/docker-to-slack/images/docs/webhooks/functions/select-function-on-output.png) Push new image to DockerHub, you should see a new notification in your Slack channel: ![Slack channel msg](https://webhookrelay.com/docs/tutorials/transform/docker-to-slack/images/docs/webhooks/functions/slack-chan-msg.png) That's it, feel free to continue modifying Lua function to include pusher's name and message format. Following this process you can transform any webhook into any other webhook. Have fun! Did this page help you? --- --- title: TradingView Webhooks to Discord & Slack | WebhookRelay meta: "og: title": "TradingView Webhooks to Discord & Slack" description: Send TradingView alerts anywhere with webhooks. Step-by-step setup to forward alerts to Discord, Slack, email or a trading API, with payload formatting. url: https://webhookrelay.com/blog/trading-view.md file: /blog/trading-view.md --- ![Stripes](https://webhookrelay.com/blog/trading-view/images/stripes.svg) # **TradingView Webhooks: Automate Alerts to Discord, Slack & More** Send TradingView alerts anywhere with webhooks. Step-by-step setup to forward alerts to Discord, Slack, email or a trading API, with payload formatting. [TradingView](https://www.tradingview.com/) is the charting and alerting platform most traders live in. Its **webhook alerts** let you push a notification to any URL the moment a condition triggers — a price cross, an indicator signal, a strategy entry. The catch: TradingView sends a plain message to one URL, and most destinations (Discord, Slack, your own API) expect a specific payload shape. Webhook Relay sits in the middle: it receives the TradingView alert, reshapes it for the destination, and can fan it out to several places at once. ## [What you'll build](#what-youll-build) TradingView alert → Webhook Relay (transform) → Discord / Slack / email / trading API. You need a paid TradingView plan (webhooks aren't on the free tier) and a free Webhook Relay account. ## [1. Get your destination URL (Discord example)](#_1-get-your-destination-url-discord-example) In Discord, open **Server Settings → Integrations → Webhooks → New Webhook**, copy the webhook URL. ![Create discord webhook](https://webhookrelay.com/blog/trading-view/images/blog/tradingview/discord.png) ## [2. Create a Webhook Relay endpoint](#_2-create-a-webhook-relay-endpoint) Open [the new public destination page](https://my.webhookrelay.com/new-public-destination) and add the Discord URL as the destination: ![Create a new public forwarding configuration](https://webhookrelay.com/blog/trading-view/images/blog/tradingview/public-url.png) Copy the **input URL** Webhook Relay gives you — that's what goes into TradingView. ![Discord webhook URL goes into the public URL as the destination](https://webhookrelay.com/blog/trading-view/images/blog/tradingview/public-url-discord.png) ## [3. Transform the payload for the destination](#_3-transform-the-payload-for-the-destination) TradingView posts your alert message as the body, but Discord expects `{ "content": "..." }`. Add a small [transformation](https://webhookrelay.com/blog/trading-view/features/transform-webhooks/). Click **Transform** on the input: ![Transform input](https://webhookrelay.com/blog/trading-view/images/blog/tradingview/transform.png) Open the [Functions](https://my.webhookrelay.com/functions) page, create a function "from scratch", and paste: ``` local json = require("json") local new_body = { content = r.RequestBody, } local encoded_payload, err = json.encode(new_body) if err then error(err) end r:SetRequestHeader("content-type", "application/json") r:SetRequestMethod("POST") r:SetRequestBody(encoded_payload) ``` This wraps the raw alert into Discord's format. You can add any logic here — parse fields, add emojis, route by ticker, or drop alerts that don't matter. ## [4. Create the TradingView alert](#_4-create-the-tradingview-alert) In TradingView, create an alert (here, BTC price — but it works for NVDA, the S&P, or any symbol): ![Creating an alert](https://webhookrelay.com/blog/trading-view/images/blog/tradingview/tradingview-alert.png) In the alert dialog, enable **Webhook URL** and paste your Webhook Relay input URL. In the **Message** field, write whatever you want delivered. You can use TradingView placeholders to include live values: ``` {{ticker}} crossed {{close}} at {{time}} — signal: {{strategy.order.action}} ``` Send a test and the alert lands in Discord: ![Discord message](https://webhookrelay.com/blog/trading-view/images/blog/tradingview/message.png) ## [Send the same alert elsewhere](#send-the-same-alert-elsewhere) The same pattern works for any destination — only the transform changes: - **Slack:** wrap the message as `{ "text": "..." }` and use your Slack incoming webhook as the destination. See [transforming webhooks](https://webhookrelay.com/blog/trading-view/features/transform-webhooks/). - **Email:** send the alert as an email from the function. - **Multiple destinations at once:** add several outputs so one alert hits Discord, Slack and email together — see [forward to multiple destinations](https://webhookrelay.com/blog/trading-view/features/webhook-multiple-destinations/). - **Your trading API / broker:** call any HTTP endpoint from the function to place an order. Test carefully before automating live trades. ## [Formatting tip: send JSON from TradingView](#formatting-tip-send-json-from-tradingview) Instead of plain text, you can put JSON directly in the alert message so your function gets structured fields: ``` {"ticker": "{{ticker}}", "action": "{{strategy.order.action}}", "price": {{close}}} ``` Then parse it in the transform and build a rich Discord/Slack message or an API call. ## [Troubleshooting](#troubleshooting) - **Alert fires but nothing arrives:** confirm the webhook URL is the Webhook Relay **input** URL, and check the request log in your dashboard. - **Discord/Slack rejects the message:** the body isn't in the expected shape — add or fix the transform. - **Webhook option greyed out:** webhooks require a paid TradingView plan. ## [Next steps](#next-steps) [Create a free account](https://my.webhookrelay.com/register) to set this up, or [inspect a TradingView alert payload](https://webhookrelay.com/blog/trading-view/webhook-bin/) first to see exactly what it sends. --- --- title: Controlling TV with Google Home, IFTTT and Node-RED meta: "og: title": "Controlling TV with Google Home, IFTTT and Node-RED" description: Easiest way to start controlling your TV with Google Home, IFTTT and Node-RED url: https://webhookrelay.com/blog/google-home-ifttt-node-red.md file: /blog/google-home-ifttt-node-red.md --- ![Stripes](https://webhookrelay.com/blog/google-home-ifttt-node-red/images/stripes.svg) # **Controlling TV with Google Home, IFTTT and Node-RED** Easiest way to start controlling your TV with Google Home, IFTTT and Node-RED ![cover of node-red, ifttt and webhook relay](https://webhookrelay.com/blog/google-home-ifttt-node-red/images/blog/ghome-ifttt-nr-tv/cover.png) In this article, I will show you how easy and quick it is to add voice controlled commands to your home/office or any other environment. Once you set up your first flow, any other features on top of that won't take even 1 minute to add. We will be using: - [Node-RED](https://nodered.org/) our main tool to wire everything together. - [Google Home](https://assistant.google.com/) - I am using Google Home Mini to launch tasks. - [IFTTT](https://ifttt.com/) will be transforming commands coming from Google Home into webhooks. - [Webhook Relay](https://webhookrelay.com) is going to act as a broker to deliver webhooks to our Raspberry PI running Node-RED without exposing it to the internet. Webhook Relay node removes a lot of work that is required to securely expose your Node-RED to the internet. It is especially useful when you can't receive webhooks in your local network due to: - ISP blocks incoming connections - Double NAT due to using 4G - No static IP - Lack of knowledge how to set up HTTPS and reverse proxy In short, it provides an encrypted, one-way transport to your Node-RED through a single node. And we do have a free tier. ## [1. Preparing to receive webhooks from IFTTT](#_1-preparing-to-receive-webhooks-from-ifttt) Webhook Relay will be acting as a message broker between Google Home with IFTTT and Node-RED. Naturally, let's configure it first. Got to [buckets page](https://my.webhookrelay.com/buckets) and create a new bucket called 'gactions': ![Webhook Relay bucket](https://webhookrelay.com/blog/google-home-ifttt-node-red/images/blog/ghome-ifttt-nr-tv/gactions-bucket.png) In the bucket details page, you should see 'Default public endpoint' URL that starts with `https://my.webhookrelay.com/v1/webhooks/...`. Keep this tab open, you will need to copy that URL into IFTTT. ## [2. Set up Google Home with IFTTT](#_2-set-up-google-home-with-ifttt) Head to [IFTTT](https://ifttt.com/), then to [your applets](https://ifttt.com/my_applets) and click on "New Applet". Search for "Google Assistant": ![IFTTT Google Assistant](https://webhookrelay.com/blog/google-home-ifttt-node-red/images/blog/ghome-ifttt-nr-tv/ifttt-g-assistant.png) **IF THIS (Google Assistant)** When choosing a trigger pick 'Say a simple phrase' for our scenario. You can try other ones later for different automation. Now, in 'What do you want to say?' section type 'turn the TV on' or something similar. Whatever you want basically. Populate other fields and pick your response phrase. Click "Create trigger". **THEN THAT (Webhook)** For the action service, choose webhook: ![IFTTT Google Assistant](https://webhookrelay.com/blog/google-home-ifttt-node-red/images/blog/ghome-ifttt-nr-tv/ifttt-webhook.png) In the URL part, take the 'Default public endpoint' from step 1 (that starts with `https://my.webhookrelay.com/v1/webhooks/...`). Choose method to "POST", set Content Type to 'application/json' and set body to: ``` { "action": "tv_on" } ``` Once done, click "Create Action". You can now repeat the same process for more commands such as turning the TV off, mute, lower the sound and so on. I have configured three applets in total to send webhooks to the same endpoint: To turn it off: ``` { "action": "tv_off" } ``` To mute it: ``` { "action": "tv_mute" } ``` > Adding new commands is very fast, takes less than a minute once you have finished with the first one. ## [3. Configure Node-RED](#_3-configure-node-red) ![Node-RED flow](https://webhookrelay.com/blog/google-home-ifttt-node-red/images/blog/ghome-ifttt-nr-tv/nr-flow.png) Our Node-RED flow consists of three steps: 1. Receive webhooks through [node-red-contrib-webhookrelay](https://flows.nodered.org/node/node-red-contrib-webhookrelay) node. 2. Extract body from the webhook and parse it using simple function and JSON node. 3. Launch actions based on switch node. To control TV I am using [node-red-contrib-tv-bravia](https://flows.nodered.org/node/node-red-contrib-tv-bravia) node. Pretty much any internet connected device will also listen to [node-red-node-wol](https://flows.nodered.org/node/node-red-node-wol) (Wake on LAN). The flow can be found on [GitHub gist here](https://gist.github.com/rusenask/c4686f64616efd2f73bbd1a8b9ecb0b0). You can either import it or add nodes one by one. For the learning purposes, I would advise to add them manually so you can better understand how it works. Let's get started. First, get [authentication tokens](https://my.webhookrelay.com/tokens) and set them to the `node-red-contrib-webhookrelay` node. In the "Buckets" field, add our "gactions" bucket which we created previously: ![Webhook Relay node configuration](https://webhookrelay.com/blog/google-home-ifttt-node-red/images/blog/ghome-ifttt-nr-tv/webhookrelay-config.png) Now, we need to extract body and parse it. Create a **function** node and in the function body add this: ``` return { payload: msg.payload.body }; ``` This will extract the webhook body from the whole webhook message (it includes input, bucket metadata, as well as request method and headers). Then, add a **json** node and configure it with: - **Action:** Convert between JSON String & Object - **Property:** msg.payload ### [Launching actions based on payload](#launching-actions-based-on-payload) Finally, time to add the switch and main control nodes. Remember the payloads that we configured in IFTTT? Time to read that action value: ![switch node configuration](https://webhookrelay.com/blog/google-home-ifttt-node-red/images/blog/ghome-ifttt-nr-tv/switch-node.png) For the control I am using [node-red-contrib-tv-bravia](https://flows.nodered.org/node/node-red-contrib-tv-bravia) node. Follow their instructions to set up the TV. In short - you need to know the IP address of your TV and MAC (for the Wake on LAN node). You can either find it from your router or go to your TV network settings and pick it up from there. Each switch wire goes to a different action, this makes it simple and robust. ## [Future work](#future-work) Once you have the flow ready up to the **switch** node, feel free to add more gadgets behind it. As you can see, IFTTT makes it super easy to issue commands and act based on those payloads in your flow. If you have your Node-RED exposed to the internet, you can even skip Webhook Relay node and receive webhooks directly through an `http` node. --- --- title: Webhook vs WebSocket: What's the Difference? | WebhookRelay meta: "og: title": "Webhook vs WebSocket: What's the Difference?" description: Webhooks and WebSockets both deliver real-time data, but one is a short one-way callback and the other a persistent connection. When to use each. url: https://webhookrelay.com/blog/webhook-vs-websocket.md file: /blog/webhook-vs-websocket.md --- ![Stripes](https://webhookrelay.com/blog/webhook-vs-websocket/images/stripes.svg) # **Webhook vs WebSocket: What's the Difference?** Webhooks and WebSockets both deliver real-time data, but one is a short one-way callback and the other a persistent connection. When to use each. Both **webhooks** and **WebSockets** are ways to get data in near real time instead of polling — but they're built for different jobs. One is a brief one-directional HTTP callback; the other is a long-lived two-way pipe. ## [The one-line answer](#the-one-line-answer) - **Webhook:** a server sends you a single **HTTP POST** when an event happens, then the connection closes. Great for **server-to-server** event notifications. - **WebSocket:** a **persistent, two-way connection** stays open so either side can send messages at any time. Great for **live, bidirectional** communication — usually browser ↔ server. ## [How a webhook works](#how-a-webhook-works) A webhook is an ordinary HTTP request. You register a URL with a provider, and they POST to it the moment something happens — then it's done. (New to them? See [what is a webhook](https://webhookrelay.com/blog/webhook-vs-websocket/blog/what-is-webhook/).) ``` POST /your-webhook-endpoint Content-Type: application/json { "event": "payment.succeeded", "id": "pi_123" } ``` There's no connection to keep alive. It's stateless, one-way (provider → you), and each event is an independent request the provider can retry if it fails. ## [How a WebSocket works](#how-a-websocket-works) A WebSocket starts as an HTTP request that "upgrades" into a persistent TCP connection which stays open. After the handshake, **both sides** can push messages whenever they like, with very little per-message overhead: ``` Client --- handshake (HTTP Upgrade) ---> Server Client <========= open connection ========> Server (either side sends messages any time) ``` That open channel is what makes WebSockets great for chat, multiplayer, live dashboards and collaborative editing — but it also means you have to manage connection state, reconnects and scaling those long-lived connections. ## [Webhook vs WebSocket at a glance](#webhook-vs-websocket-at-a-glance) | | Webhook | WebSocket | | --- | --- | --- | | Connection | Short-lived HTTP request per event | Persistent, always-open | | Direction | One-way (server → you) | Two-way (full duplex) | | Who connects | Provider POSTs to your URL | Client opens the connection to a server | | Best for | Server-to-server event notifications | Live browser ↔ server messaging | | Overhead | New request each event | One connection, low per-message cost | | Delivery on failure | Provider **retries** the POST | You handle reconnection & missed messages | | Needs a public URL | Yes — a URL the provider can reach | No — the client dials out | | Typical examples | Stripe payments, GitHub pushes, CI/CD | Chat, live dashboards, multiplayer | ## [When to use which](#when-to-use-which) **Reach for a webhook when:** - One backend needs to tell another backend that an event happened (a payment, a deploy, a new record). - You want the provider to own delivery and retries. - You don't want to maintain an always-on connection. **Reach for a WebSocket when:** - A browser (or client) needs live updates _and_ needs to send data back. - You're streaming many messages continuously and want minimal per-message overhead. - Latency on a steady stream matters — chat, presence, live prices, collaborative UIs. ## [They're better together](#theyre-better-together) A very common architecture uses **both**: your server receives **webhooks** from providers, then fans the relevant updates out to connected browsers over **WebSockets**. ``` Stripe --webhook--> your backend --WebSocket--> user's browser (live "payment received!") ``` The webhook is the reliable server-to-server event; the WebSocket is the live last hop to the UI. ## [The catch with webhooks: you need a reachable URL](#the-catch-with-webhooks-you-need-a-reachable-url) Unlike a WebSocket, where the client dials out, a webhook needs a **public URL the provider can POST to** — awkward when your handler runs on `localhost` or behind a firewall. That's what Webhook Relay solves: [inspect the payload](https://webhookrelay.com/blog/webhook-vs-websocket/webhook-bin/) in your browser, then [forward it to localhost or a private server](https://webhookrelay.com/blog/webhook-vs-websocket/webhooks/) with no public IP. For the related comparison, see [webhooks vs API](https://webhookrelay.com/blog/webhook-vs-websocket/blog/webhooks-vs-api/). Ready to try one? [Test a webhook now](https://webhookrelay.com/blog/webhook-vs-websocket/webhook-bin/) or [start forwarding for free](https://my.webhookrelay.com/register). --- --- title: Receive Shopify webhooks on Flask API | WebhookRelay meta: "og: title": "Receive Shopify webhooks on Flask API" description: How to get Shopify webhooks working on your local Flask app, with proper signature verification url: https://webhookrelay.com/blog/receiving-shopify-webhooks-flask-api.md file: /blog/receiving-shopify-webhooks-flask-api.md --- ![Stripes](https://webhookrelay.com/blog/receiving-shopify-webhooks-flask-api/images/stripes.svg) # **Receive Shopify webhooks on Flask API** How to get Shopify webhooks working on your local Flask app, with proper signature verification If you're building a Shopify integration, you'll need to receive webhooks. The problem is Shopify can't reach your laptop. This post shows how to use Webhook Relay to forward webhooks to your local Flask app, with proper HMAC verification so you don't get burned by fake requests in production. ## [What you'll need](#what-youll-need) - A [Webhook Relay account](https://my.webhookrelay.com/register) - Python 3.8+ - The [relay CLI](https://webhookrelay.com/blog/receiving-shopify-webhooks-flask-api/docs/installation/cli/) ## [Set up the Flask project](#set-up-the-flask-project) I'm using `uv` here because it's fast, but pip works fine too. ``` curl -LsSf https://astral.sh/uv/install.sh | sh mkdir flask-webhook-server cd flask-webhook-server uv venv source .venv/bin/activate uv pip install flask ``` ## [The Flask app](#the-flask-app) Here's the full app. The important part is verifying the HMAC signature before you trust anything in the payload. Shopify signs every webhook with your store's secret, and you should check it. If your check keeps failing, paste the captured body and secret into the [Shopify signature verifier](https://webhookrelay.com/blog/receiving-shopify-webhooks-flask-api/verify-shopify-webhook-signature/) to see what the digest _should_ be. Create `app.py`: ``` import hmac import hashlib import base64 import os from flask import Flask, request, jsonify app = Flask(__name__) # Grab this from Shopify admin: Settings > Notifications > Webhooks # Don't commit it to your repo SHOPIFY_WEBHOOK_SECRET = os.environ.get('SHOPIFY_WEBHOOK_SECRET', '') def verify_shopify_webhook(data: bytes, hmac_header: str) -> bool: """Check if the webhook actually came from Shopify.""" if not SHOPIFY_WEBHOOK_SECRET: print("ERROR: SHOPIFY_WEBHOOK_SECRET not set. Please set it:") print(" export SHOPIFY_WEBHOOK_SECRET='your-secret-from-shopify-admin'") return False calculated_hmac = base64.b64encode( hmac.new( SHOPIFY_WEBHOOK_SECRET.encode('utf-8'), data, hashlib.sha256 ).digest() ).decode('utf-8') return hmac.compare_digest(calculated_hmac, hmac_header) @app.route('/webhook', methods=['POST']) def webhook(): # Get raw body BEFORE parsing JSON - you need the exact bytes for HMAC data = request.get_data() hmac_header = request.headers.get('X-Shopify-Hmac-Sha256', '') topic = request.headers.get('X-Shopify-Topic', 'unknown') shop_domain = request.headers.get('X-Shopify-Shop-Domain', 'unknown') if not verify_shopify_webhook(data, hmac_header): print(f"Bad signature from {shop_domain}") return jsonify({"error": "Invalid signature"}), 401 payload = request.get_json() print(f"Got {topic} webhook from {shop_domain}") print(f"Payload: {payload}") # Do something with it if topic == 'orders/create': print(f"New order #{payload.get('order_number')} - ${payload.get('total_price')}") return jsonify({"status": "received"}), 200 if __name__ == '__main__': app.run(host='0.0.0.0', port=4444, debug=True) ``` A couple things to note: - You have to read the raw request body before Flask parses it. The HMAC is computed over the exact bytes Shopify sent. - `hmac.compare_digest` prevents timing attacks. Regular string comparison leaks information about which characters matched. ## [Run the server](#run-the-server) ``` export SHOPIFY_WEBHOOK_SECRET="your-secret-from-shopify-admin" python app.py ``` You'll see: ``` * Running on http://0.0.0.0:4444 ``` ## [Forward webhooks with Webhook Relay](#forward-webhooks-with-webhook-relay) Open another terminal and run: ``` relay login -k your-token-key -s your-token-secret relay forward -b shopify-webhooks http://localhost:4444/webhook ``` You'll get a public URL like `https://xxx.hooks.webhookrelay.com`. Copy that. ## [Configure Shopify](#configure-shopify) 1. Go to your Shopify admin 2. Settings > Notifications > Webhooks 3. Copy the signing secret at the top (that's your `SHOPIFY_WEBHOOK_SECRET`) 4. Click "Create webhook" 5. Pick an event, set format to JSON, paste your Webhook Relay URL 6. Save ## [Test it](#test-it) You can use Shopify's "Send test notification" button, or hit it with curl: ``` curl -X POST https://xxx.hooks.webhookrelay.com \ -H "Content-Type: application/json" \ -H "X-Shopify-Topic: orders/create" \ -H "X-Shopify-Shop-Domain: your-store.myshopify.com" \ -d '{"id": 12345, "order_number": "1001", "total_price": "99.99"}' ``` Your Flask server should print the payload. ## [Going to production](#going-to-production) For production, you'll want: - `gunicorn` instead of the Flask dev server - The Webhook Relay agent running as a service (see our [Docker guide](https://webhookrelay.com/blog/receiving-shopify-webhooks-flask-api/docs/installation/docker/)) - Or just point the Webhook Relay output directly at your public server --- --- title: Test HubSpot Webhooks Locally | WebhookRelay meta: "og: title": "Test HubSpot Webhooks Locally" description: Test HubSpot webhooks on localhost without deploying. Inspect the batched payload and verify the X-HubSpot-Signature-v3 header on every request. url: https://webhookrelay.com/blog/receive-hubspot-webhooks-locally.md file: /blog/receive-hubspot-webhooks-locally.md --- ![Stripes](https://webhookrelay.com/blog/receive-hubspot-webhooks-locally/images/stripes.svg) # **Test HubSpot Webhooks Locally** Test HubSpot webhooks on localhost without deploying. Inspect the batched payload and verify the X-HubSpot-Signature-v3 header on every request. ![Test HubSpot Webhooks Locally (Receive HubSpot Webhooks on localhost)](https://webhookrelay.com/blog/receive-hubspot-webhooks-locally/images/blog/heroes/localhost.jpg) You are building a HubSpot integration — syncing CRM contacts, reacting to a deal stage change, kicking off an automation — and you need to watch your handler react to a real `contact.creation` or `deal.propertyChange` event. The problem is immediate: HubSpot will only POST to a **public URL**, and your handler is running on `localhost:3000`. HubSpot has no way to reach it. The usual workarounds are painful. Deploying to a staging server for every change is slow. Copying a sample payload from the docs into curl gives you a _guess_ at the real request — not the real headers, the real signature, or HubSpot's batched array format. What you want is to **test HubSpot webhooks locally** — real events hitting your local handler, with a URL that doesn't change every time you restart. This guide shows how to do exactly that. ## [Why receiving HubSpot webhooks locally is tricky](#why-receiving-hubspot-webhooks-locally-is-tricky) A webhook is just an HTTP request that HubSpot sends to a URL when something happens in your CRM. HubSpot sits on the public internet; your dev machine usually does not. It's behind a router, a corporate firewall, or both, with no public IP and no inbound ports open. So you need something in the middle: a public endpoint HubSpot can hit, that relays each request down to your laptop without you opening a single firewall port. That's what [Webhook Relay](https://my.webhookrelay.com/register) does — and unlike a random tunnel URL, the endpoint is **stable**, so you configure HubSpot once and never touch it again. ## [Step 1: Inspect the real payload with Webhook Bin](#step-1-inspect-the-real-payload-with-webhook-bin) Before you write any handler code, find out what HubSpot actually sends. Open the free [Webhook Bin](https://webhookrelay.com/blog/receive-hubspot-webhooks-locally/webhook-bin/) — no signup — and you get an instant public URL. 1. Copy the Webhook Bin URL. 2. In HubSpot, open your app's **Webhooks** settings (see Step 2 for exactly where) and set this URL as the **Target URL**. 3. Subscribe to an event such as `contact.creation`, then create a contact in your portal. Inspect the captured request and you'll notice two HubSpot-specific things right away: - **The body is a JSON array.** HubSpot **batches** events, so even a single event arrives as an array of event objects. Your handler must loop over the array. - **The signature and timestamp headers.** Newer apps send `X-HubSpot-Signature-v3` together with `X-HubSpot-Request-Timestamp`. You'll verify these in Step 3. Now you know the exact shape of the data before writing a line of code. For more on this approach, see [How to test webhooks](https://webhookrelay.com/blog/receive-hubspot-webhooks-locally/blog/how-to-test-webhooks/) and [What is a webhook](https://webhookrelay.com/blog/receive-hubspot-webhooks-locally/blog/what-is-webhook/). ## [Step 2: Forward the events to localhost with the relay agent](#step-2-forward-the-events-to-localhost-with-the-relay-agent) Once you know the payload, route those same events into your local handler. [Sign up for Webhook Relay](https://my.webhookrelay.com/register), install the `relay` agent (CLI or Docker), and create a bucket — say `hubspot`. The bucket gives you a stable public input endpoint. Start forwarding to your local server: ``` relay forward --bucket hubspot http://localhost:3000/webhook ``` The agent opens an **outbound** connection to Webhook Relay and streams every incoming request down to `http://localhost:3000/webhook`. Because the connection is outbound, there are **no firewall ports to open and no public IP needed** — this works from your laptop, behind a corporate proxy, or inside a Kubernetes cluster. Running in Docker? The same command works in the official `webhookrelay/webhookrelayd` image. Full details are in the [localhost forwarding docs](https://webhookrelay.com/blog/receive-hubspot-webhooks-locally/docs/webhooks/internal/localhost/). Now update the HubSpot **Target URL** to your Webhook Relay endpoint (or just set it there from the start), trigger an event, and watch it arrive on `localhost`. ## [HubSpot-specific configuration and quirks](#hubspot-specific-configuration-and-quirks) Webhooks live inside an **app**, not in the general portal settings: - **Where to configure them:** create or open a **private app** (or a developer/public app) in HubSpot, then go to **App settings → Webhooks**. Set the **Target URL** to your Webhook Relay endpoint and add **subscriptions** for the events you care about — `contact.creation`, `contact.propertyChange`, `deal.creation`, `deal.propertyChange`, and so on across CRM objects. - **You need the right scopes.** Webhook subscriptions require the relevant CRM scopes (for example, contacts) to be granted to the app. - **Events are batched.** As noted above, the body is always a JSON **array**. Each element carries fields like `subscriptionType`, `objectId`, `propertyName`, `propertyValue`, `occurredAt`, and `eventId`. Iterate; don't assume one object. - **Concurrency and retries.** HubSpot may deliver batches concurrently and retries failed deliveries, so make your handler **idempotent** — dedupe on `eventId` — and return a `2xx` quickly. ## [Step 3: Verify the HubSpot webhook signature](#step-3-verify-the-hubspot-webhook-signature) Authenticate every request before trusting it. Newer HubSpot apps use the **`X-HubSpot-Signature-v3`** header, which is the most secure option and the only one with replay protection. Here's how v3 works: 1. **Check the timestamp.** Read `X-HubSpot-Request-Timestamp`. HubSpot timestamps are in **milliseconds**. Reject the request if it's more than **5 minutes** old — this is what stops replay attacks. 2. **Build the signed string.** Concatenate, in order: the HTTP **method** + the full request **URI** + the raw request **body** + the **timestamp**. Decode any URL-encoded characters in the URI (but leave the `?` that begins the query string). 3. **Compute the HMAC.** Take an **HMAC-SHA256** of that string using your app's **client secret** as the key. 4. **Base64-encode and compare.** Base64-encode the HMAC result and compare it to the `X-HubSpot-Signature-v3` header using a **constant-time** comparison. A subtle but critical detail: the signed string includes the **full URL HubSpot actually called** — which, in this setup, is your Webhook Relay endpoint, not `localhost`. Compute and capture the v3 signature against the public URL HubSpot saw, exactly as it arrived in the forwarded request. Older apps may still send the v1 (`X-HubSpot-Signature` as a SHA-256 over client-secret + body) or v2 schemes; v3 is the current, recommended version. To sanity-check an HMAC-SHA256 implementation quickly, paste a captured body, your secret, and the received signature into the free [HMAC signature verifier](https://webhookrelay.com/blog/receive-hubspot-webhooks-locally/hmac-verification/). For language-specific code and the common pitfalls — reading the body _after_ a JSON parser has consumed it, timing-safe comparison — read [Verify a webhook signature](https://webhookrelay.com/blog/receive-hubspot-webhooks-locally/blog/verify-webhook-signature/). ## [Replay and iterate](#replay-and-iterate) This is where local development gets fast: - **Replay from Webhook Relay** — past requests are stored on your bucket, so you can resend a captured HubSpot batch against your handler without touching HubSpot at all. - **Iterate on your handler** by editing code and replaying the same delivery until it behaves correctly. No object changes in the CRM, no deploys, just to test a code path. Because the Webhook Relay endpoint is stable, you can stop and restart the agent, reboot your machine, or come back next week — the HubSpot Target URL never needs to change. ## [Get started](#get-started) 1. Inspect the real payload in the free [Webhook Bin](https://webhookrelay.com/blog/receive-hubspot-webhooks-locally/webhook-bin/) — no signup needed. 2. [Create a Webhook Relay account](https://my.webhookrelay.com/register), install the agent, and run `relay forward --bucket hubspot http://localhost:3000/webhook`. 3. Point your HubSpot private app's Target URL at the stable endpoint, trigger an event, and watch it hit `localhost`. You'll be testing real HubSpot events against your local handler in a few minutes — no deploys, no open firewall ports, and a URL you configure exactly once. --- --- title: Test Intercom Webhooks Locally | WebhookRelay meta: "og: title": "Test Intercom Webhooks Locally" description: Test Intercom webhooks locally on localhost without deploying. Inspect the real topic payload, forward to your handler, and verify the X-Hub-Signature. url: https://webhookrelay.com/blog/receive-intercom-webhooks-locally.md file: /blog/receive-intercom-webhooks-locally.md --- ![Stripes](https://webhookrelay.com/blog/receive-intercom-webhooks-locally/images/stripes.svg) # **Test Intercom Webhooks Locally** Test Intercom webhooks locally on localhost without deploying. Inspect the real topic payload, forward to your handler, and verify the X-Hub-Signature. ![Test Intercom Webhooks Locally (Intercom Webhook on localhost)](https://webhookrelay.com/blog/receive-intercom-webhooks-locally/images/blog/heroes/localhost.jpg) You are building an Intercom integration — syncing contacts, reacting to new conversations, piping events into your own product — and you need to see your handler react to a real `conversation.user.created` or `contact.created` notification. The problem is immediate: Intercom will only POST to a **public HTTPS URL**, and your handler is running on `localhost:8080`. Intercom has no way to reach it. The usual workarounds are painful. Deploying to a staging server for every code change is slow. Copying a sample payload from the docs into curl gives you a _guess_ at the real request, not the actual `topic`, `data`, and `X-Hub-Signature` Intercom sends. What you want is to **test Intercom webhooks locally** — real notifications, hitting your local handler, with a URL that does not change every time you restart. This guide shows how to do exactly that. ## [Why testing Intercom webhooks locally is tricky](#why-testing-intercom-webhooks-locally-is-tricky) A webhook is just an HTTP request that Intercom sends to a URL when something happens in your workspace — a new conversation, a contact update, a tag applied. Intercom sits on the public internet; your dev machine usually does not. It is behind a router, a corporate firewall, or both, with no public IP and no inbound ports open. So you need something in the middle: a public HTTPS endpoint Intercom can hit, that relays each request down to your laptop without you opening a single firewall port. That is what [Webhook Relay](https://my.webhookrelay.com/register) does — and unlike a random tunnel URL, the endpoint is **stable**, so you configure Intercom once and never touch it again. There is one Intercom-specific wrinkle: when you save a webhook URL in the Developer Hub, Intercom sends a **HEAD request to validate it** before it will accept the subscription. Your endpoint must respond successfully to that check, and it must be **HTTPS**. Webhook Relay endpoints are HTTPS and answer the validation probe, so the URL is accepted right away. ## [Step 1: Inspect the real payload with Webhook Bin](#step-1-inspect-the-real-payload-with-webhook-bin) Before you write any handler code, find out what Intercom actually sends. Open the free [Webhook Bin](https://webhookrelay.com/blog/receive-intercom-webhooks-locally/webhook-bin/) — no signup — and you get an instant public HTTPS URL. 1. Copy the Webhook Bin URL. 2. Go to the [Intercom Developer Hub](https://app.intercom.com/a/apps/_/developer-hub), open your app, and in **Configure → Webhooks** paste the URL as the endpoint. 3. Subscribe to the topics you care about — for example `conversation.user.created`, `contact.created`, or `contact.tag.created`. 4. Save. Intercom validates the URL with a HEAD request, then starts delivering notifications. Trigger a real action in your workspace (start a conversation, create a contact) and inspect the captured request in Webhook Bin: the full JSON body, the query string, and every header. You will see the notification envelope Intercom uses — `type: notification_event`, the `topic`, your `app_id`, and a `data` object wrapping the affected Intercom resource — plus the `X-Hub-Signature` header. Now you know the exact shape of the data before writing a line of code. For more on this approach, see [How to test webhooks](https://webhookrelay.com/blog/receive-intercom-webhooks-locally/blog/how-to-test-webhooks/) and [What is a webhook](https://webhookrelay.com/blog/receive-intercom-webhooks-locally/blog/what-is-webhook/). ## [Step 2: Forward the notifications to localhost with the relay agent](#step-2-forward-the-notifications-to-localhost-with-the-relay-agent) Once you know the payload, route those same notifications into your local handler. [Sign up for Webhook Relay](https://my.webhookrelay.com/register), install the `relay` agent (CLI or Docker), and create a bucket — say `intercom`. The bucket gives you a stable public input endpoint. Start forwarding to your local server: ``` relay forward --bucket intercom http://localhost:8080/webhook ``` The agent opens an **outbound** connection to Webhook Relay and streams every incoming request down to `http://localhost:8080/webhook`. Because the connection is outbound, there are **no firewall ports to open and no public IP needed** — this works from your laptop, behind a corporate proxy, or inside a Kubernetes cluster. Running in Docker? The same command works in the official `webhookrelay/webhookrelayd` image. Full details are in the [localhost forwarding docs](https://webhookrelay.com/blog/receive-intercom-webhooks-locally/docs/webhooks/internal/localhost/). Now set the Intercom webhook endpoint in **Configure → Webhooks** to your Webhook Relay endpoint (or just create it there from the start). Trigger an event and watch it arrive on `localhost`. ## [Intercom-specific configuration and quirks](#intercom-specific-configuration-and-quirks) A few Intercom details worth knowing: - **Where to configure it:** webhooks live on a **Developer Hub app**, not in the main workspace settings. Open your app and go to **Configure → Webhooks**. You also need the matching permissions enabled on the app's Authorization page before some topics will deliver. - **Topics, not "all events":** you explicitly subscribe to topics such as `conversation.user.created`, `conversation.admin.replied`, `contact.created`, and `contact.tag.created`. Use the `topic` field in the body to branch in your handler. - **HTTPS + HEAD validation:** the endpoint must be HTTPS and must answer Intercom's HEAD validation request, or the subscription will not save. Webhook Relay handles both. - **Respond fast with 200:** Intercom expects an HTTP `200` within **5 seconds** and will **retry** otherwise. Acknowledge receipt immediately, then do slow work asynchronously. Sustained error responses will pause or suspend your subscription. - **Payload envelope:** the body always wraps the resource in a `notification_event` shape with a `data.item` object — your handler reads `topic` to know what happened and `data.item` for the resource. ## [Step 3: Verify the Intercom webhook signature](#step-3-verify-the-intercom-webhook-signature) Intercom signs every notification so you can confirm it really came from Intercom. It computes an **HMAC-SHA1** of the raw request body using your **app's client secret** as the key, hex-encodes the result, and sends it in the **`X-Hub-Signature`** header, formatted as `sha1=...`. Your handler should recompute the HMAC-SHA1 over the **raw** body (the exact bytes received, before any JSON parser reshapes them) and compare in constant time before trusting the payload. Note this is **SHA-1**, not the SHA-256 some other providers use — match the algorithm exactly or the digest will never line up. To sanity-check your implementation, paste a captured body, your client secret, and the received signature into the free [HMAC signature verifier](https://webhookrelay.com/blog/receive-intercom-webhooks-locally/hmac-verification/). For language-specific code and the common pitfalls (reading the body after a JSON parser has consumed it, timing-safe comparison), read [Verify a webhook signature](https://webhookrelay.com/blog/receive-intercom-webhooks-locally/blog/verify-webhook-signature/). ## [Replay and iterate](#replay-and-iterate) This is where local development gets fast: - **Replay from Webhook Relay** — past requests are stored on your bucket, so you can resend a captured notification against your handler without touching Intercom at all. - **Iterate on your handler** by editing code and replaying the same delivery until it behaves correctly. No deploys just to test a single code path. Because the Webhook Relay endpoint is stable, you can stop and restart the agent, reboot your machine, or come back next week — the Intercom configuration never needs to change. ## [Get started](#get-started) 1. Inspect the real payload in the free [Webhook Bin](https://webhookrelay.com/blog/receive-intercom-webhooks-locally/webhook-bin/) — no signup needed. 2. [Create a Webhook Relay account](https://my.webhookrelay.com/register), install the agent, and run `relay forward --bucket intercom http://localhost:8080/webhook`. 3. Point your Intercom Developer Hub webhook at the stable endpoint, trigger an event, and watch it hit `localhost`. You will be testing real Intercom topics against your local handler in a few minutes — no deploys, no open firewall ports, and a URL you configure exactly once. --- --- title: Receive Github webhooks on Jenkins without public IP meta: "og: title": "Receive Github webhooks on Jenkins without public IP" description: A short tutorial on how to configure and receive Github webhooks on your jenkins instance even without a public IP url: https://webhookrelay.com/blog/github-jenkins-guide.md file: /blog/github-jenkins-guide.md --- ![Stripes](https://webhookrelay.com/blog/github-jenkins-guide/images/stripes.svg) # **Receive Github webhooks on Jenkins without public IP** A short tutorial on how to configure and receive Github webhooks on your jenkins instance even without a public IP ## [How to Set Up Jenkins GitHub Webhooks](#how-to-set-up-jenkins-github-webhooks) To set up Jenkins GitHub webhooks, you need to install the GitHub plugin in Jenkins, configure a webhook URL in your GitHub repository settings, and set your Jenkins job to trigger on GitHub hook events. If Jenkins is behind a firewall, you can use Webhook Relay to receive webhooks without exposing your server to the public internet. [Jenkins](https://jenkins.io/) is probably the most popular CI tool. Since Dev and Ops roles are growing more and more alike we need to reduce initially required 'activation energy' to adopt automation tools. Building and testing software should be easy, fast and reliable. Current technique known as _polling_ repository usually results in constant delays where engineers have to wait till the next CI scanning cycle. Another option is to use webhooks but that requires you to expose your CI server to the public internet (you can still whitelist IPs though). In this article we are going to show how to configure end-to-end CI pipeline where you can instantly trigger jobs via Webhook Relay service without internal Jenkins being exposed to public internet. > If you are running Jenkins on Kubernetes, check my other blog post [here](https://webhookrelay.com/blog/github-jenkins-guide/blog/2018/12/18/webhooks-to-jenkins-on-kubernetes/). It demonstrates how to configure a Jenkins instance with Webhook Relay agent running as a sidecar. --- > For production-ready Webhook Relay & Jenkins setup please visit our [Jenkins tutorial](https://webhookrelay.com/blog/github-jenkins-guide/docs/tutorials/cicd/jenkins-github/). This tutorial will show how to configure agent in a way that it survives restarts either with Docker container or with operating system background service. ## [Desired workflow](#desired-workflow) Let's say we need to connect GitHub with internally deployed Jenkins which is not reachable from outside. In order to do this, we will be using Webhook Relay: ![desired workflow](https://webhookrelay.com/blog/github-jenkins-guide/images/blog/jenkins-guide/jenkins-github-webhookrelay.png) Once webhook is relayed by the agent, Jenkins pulls the newest code and executes the configured job. ## [Step 1: Create GitHub repository](#step-1-create-github-repository) First things first, we need to get a repository! If you haven't got [GitHub](https://github.com/) account, get one. You will need it later to login to Webhook Relay too. Once you have logged into GitHub, look for a green "New Repository" button on the bottom right corner and click it. ![creating new github repo](https://webhookrelay.com/blog/github-jenkins-guide/images/blog/jenkins-guide/github-create-repo.png) Choose public option as it will make this tutorial slightly quicker. Don't close this repo window as we will be back there shortly. ## [Step 2: Jenkins Installation (if you already have it - ignore this step)](#step-2-jenkins-installation-if-you-already-have-it-ignore-this-step) It's time to get that Jenkins up and running! Head to [https://jenkins.io/download/](https://jenkins.io/download/) and download our beloved (it's not pretty but gets the job done) Jenkins. In this guide we use standard installation but I would recommend using dockerized images, especially since it's so easy to build a new image on top of the official one. ## [Step 3: Setting up Jenkins with Github-Plugin](#step-3-setting-up-jenkins-with-github-plugin) The easiest way to start receiving GitHub webhooks is by using this plugin [https://plugins.jenkins.io/github](https://plugins.jenkins.io/github): - Go to your Jenkins plugin manager - Find and install "GitHub plugin" (at the time of writing - current version was 1.27.0) - Once it's installed, we will need to configure it: ![manage jenkins](https://webhookrelay.com/blog/github-jenkins-guide/images/blog/jenkins-guide/jenkins-manage.png) Add default GitHub server (don't bother adding credentials as we are using public repo anyway): ![jenkins add gh](https://webhookrelay.com/blog/github-jenkins-guide/images/blog/jenkins-guide/jenkins-add-gh.png) ## [Step 4: Configuring Jenkins Job](#step-4-configuring-jenkins-job) When you want Jenkins to do something - create a job. In this case we will be using **Freestyle project**: ![jenkins create job](https://webhookrelay.com/blog/github-jenkins-guide/images/blog/jenkins-guide/jenkins-new-job.png) We have to configure several sections here - **Source Code Management** and **Build Triggers**. First, set repository (in this case it's my demo app repo [repository](https://github.com/rusenask/jenkins-whr-demo)): ![jenkins create job](https://webhookrelay.com/blog/github-jenkins-guide/images/blog/jenkins-guide/jenkins-code-management.png) Next step is setting a build trigger to **GitHub hook trigger for GITScm polling**: ![jenkins build trigger](https://webhookrelay.com/blog/github-jenkins-guide/images/blog/jenkins-guide/jenkins-build-triggers.png) This means that once the Jenkins receives a webhook, it can identify which repo is changed and thus triggers a pull and job execution. ## [Step 5: Configuring Webhook Relay](#step-5-configuring-webhook-relay) First of all you will have to register and download the agent. See [installation instructions](https://docs.webhookrelay.com/installation-options/installation-options/install-cli) if you don't have an account yet (you can use GitHub OAuth option since you would already have an account with it). For authentication, agents use pairs of keys and secrets or regular account usernames/passwords. Head to the [tokens](https://my.webhookrelay.com/tokens) page (left navigation menu) and create one key/secret pair. Be sure to copy those somewhere so you don't lose them in the next 2 minutes. ## [Step 6: Setting up Webhook Relay agent](#step-6-setting-up-webhook-relay-agent) To login with the CLI use token (generate it [here](https://my.webhookrelay.com/tokens)) key/secret: ``` relay login -k your-token-key -s your-token-secret ``` You will then need to start forwarding webhooks to Jenkins: ``` relay forward --bucket github-jenkins http://localhost:8080/github-webhook/ Forwarding: https://my.webhookrelay.com/v1/webhooks/6edf55c7-e774-46f8-a058-f4d9c527a6a7 -> http://localhost:8080/github-webhook/ Starting webhook relay agent... 1.511438424864371e+09 info webhook relay ready... {"host": "api.webhookrelay.com:8080"} ``` Once you have your **Input** URL (in this case it's [https://my.webhookrelay.com/v1/webhooks/6edf55c7-e774-46f8-a058-f4d9c527a6a7](https://my.webhookrelay.com/v1/webhooks/6edf55c7-e774-46f8-a058-f4d9c527a6a7)) get back to GitHub's webhook configuration and set it: ![github webhook config](https://webhookrelay.com/blog/github-jenkins-guide/images/blog/jenkins-guide/github-webhook-configuration.png) That's it! The agent is running and forwarding requests. ## [Create sample application](#create-sample-application) For the sake of demo I have created a sample application, source code [here](https://github.com/rusenask/jenkins-whr-demo). It has some tests inside that our Jenkins job will be able to run. I use [EnvInject Plugin](https://wiki.jenkins-ci.org/display/JENKINS/EnvInject+Plugin) to provide access to system's Go. The last thing left, is to add build step: ![jenkins build](https://webhookrelay.com/blog/github-jenkins-guide/images/blog/jenkins-guide/jenkins-build.png) On the Webhook Relay logs page [https://my.webhookrelay.com/logs](https://my.webhookrelay.com/logs) you should be a able to see sent logs. ## [Test it!](#test-it) Now, do some changes and push to master. In a few seconds you should see that Jenkins is checking repository. Cheers! ## [Step-by-Step Setup Guide (short version)](#step-by-step-setup-guide-short-version) Here's a complete step-by-step guide to set up Jenkins GitHub webhooks: 1. **Install Jenkins GitHub Plugin** - Go to Jenkins plugin manager and install the "GitHub plugin" 2. **Configure GitHub Server** - In Jenkins settings, add the default GitHub server configuration 3. **Create Jenkins Job** - Set up a Freestyle project with your repository URL in Source Code Management 4. **Enable Webhook Trigger** - Select "GitHub hook trigger for GITScm polling" in Build Triggers 5. **Set Up Webhook Relay** - Sign up for Webhook Relay and create an authentication token 6. **Start Forwarding** - Run the relay agent to forward webhooks from Webhook Relay to your Jenkins instance 7. **Configure GitHub Webhook** - Add the Webhook Relay URL to your GitHub repository webhook settings 8. **Test the Integration** - Push a commit to your repository and verify Jenkins triggers the build ## [Troubleshooting FAQ](#troubleshooting-faq) ### [Webhook is received but Jenkins doesn't trigger the build](#webhook-is-received-but-jenkins-doesnt-trigger-the-build) Check that your Jenkins job has the correct repository URL configured in the Source Code Management section. The repository URL must exactly match what GitHub sends in the webhook payload. ### [Webhook Relay agent keeps disconnecting](#webhook-relay-agent-keeps-disconnecting) Ensure your token key and secret are correct. You can generate a new token from the [Webhook Relay tokens page](https://my.webhookrelay.com/tokens). For production use, run the agent as a system service. ### [GitHub shows webhook delivery failed](#github-shows-webhook-delivery-failed) Verify that the Webhook Relay agent is running and forwarding to the correct Jenkins URL. Check the Webhook Relay logs page to see if webhooks are being received and forwarded successfully. ### [Jenkins is not pulling the latest code](#jenkins-is-not-pulling-the-latest-code) Make sure the "GitHub hook trigger for GITScm polling" option is enabled in your Jenkins job configuration. Also verify that Jenkins has proper Git credentials configured if you're using a private repository. ## [Wrapping it up](#wrapping-it-up) Webhooks are not a new thing. A lot of public services use them to notify one another about certain events. The tricky part, however, is when you need them during development or when you have services that are not accessible through the public internet. That's where Webhook Relay offers a straight forward solution to the problem. P.S. If you are running Jenkins on Kubernetes, check my other blog post [here](https://webhookrelay.com/blog/github-jenkins-guide/blog/2018/12/18/webhooks-to-jenkins-on-kubernetes/). --- --- title: How to Get a Slack Webhook URL (Incoming Webhooks) meta: "og: title": "How to Get a Slack Webhook URL (Incoming Webhooks)" description: Get a Slack incoming webhook URL step by step: create a Slack app, enable Incoming Webhooks, pick a channel, and post with curl. Includes the format. url: https://webhookrelay.com/blog/how-to-get-slack-webhook-url.md file: /blog/how-to-get-slack-webhook-url.md --- ![Stripes](https://webhookrelay.com/blog/how-to-get-slack-webhook-url/images/stripes.svg) # **How to Get a Slack Webhook URL (Incoming Webhooks)** Get a Slack incoming webhook URL step by step: create a Slack app, enable Incoming Webhooks, pick a channel, and post with curl. Includes the format. A **Slack incoming webhook URL** lets any app or script post messages into a Slack channel with a simple HTTP request — no Slack SDK required. This guide gets you a URL in a couple of minutes, shows the exact format, and covers how to send messages and a no-app alternative. (New to webhooks in general? See [what is a webhook](https://webhookrelay.com/blog/how-to-get-slack-webhook-url/blog/what-is-webhook/).) ## [What a Slack webhook URL looks like](#what-a-slack-webhook-url-looks-like) An incoming webhook URL has three parts — a workspace ID, an app/webhook ID, and a secret token: ``` https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX ``` Anyone who has the full URL can post to that channel, so **treat it like a password** — don't commit it to a public repo. ## [Get a Slack webhook URL (Incoming Webhooks)](#get-a-slack-webhook-url-incoming-webhooks) Slack exposes incoming webhooks through a **Slack app**. It sounds heavier than it is: 1. **Create an app.** Go to [api.slack.com/apps](https://api.slack.com/apps) → **Create New App** → **From scratch**. Give it a name and pick the workspace, then **Create App**. 2. **Enable Incoming Webhooks.** In the app's left sidebar, open **Features → Incoming Webhooks** and toggle **Activate Incoming Webhooks** to **On**. 3. **Add a webhook to the workspace.** Scroll down and click **Add New Webhook to Workspace**. Choose the **channel** the webhook should post to and click **Allow**. 4. **Copy the URL.** Slack drops you back on the Incoming Webhooks page with your new URL listed under **Webhook URLs for Your Workspace**. Copy it. That URL posts to exactly the channel you chose. Need another channel? Repeat step 3 to add a second webhook and you'll get a second URL. ![Activating Incoming Webhooks in a Slack app — the toggle switched On and the "Add New Webhook to Workspace" button under "Webhook URLs for Your Workspace"](https://webhookrelay.com/blog/how-to-get-slack-webhook-url/images/guides/slack-incoming-webhooks.png) ## [Send a test message](#send-a-test-message) Post any JSON with a `text` field: ``` curl -X POST -H 'Content-type: application/json' \ --data '{"text":"Hello from an incoming webhook :wave:"}' \ https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXX ``` For richer messages — buttons, sections, fields — send `blocks` built with Slack's [Block Kit](https://api.slack.com/block-kit) instead of plain `text`. ## [Alternative: Workflow Builder (no app)](#alternative-workflow-builder-no-app) If you'd rather not create an app, Slack's **Workflow Builder** can generate a webhook trigger: create a workflow, choose **From a webhook** as the trigger, and Workflow Builder gives you a URL plus a step to post a message. It's handy for no-code posting, though it's less flexible than an app-based incoming webhook and depends on your Slack plan. Verify availability for your workspace. ## [Sending _any_ event to Slack](#sending-any-event-to-slack) An incoming webhook is only half the story — you still need something to POST to it in the right shape. That's where Webhook Relay fits: point a provider (Stripe, GitHub, a monitoring alert) at Webhook Relay, [transform the payload](https://webhookrelay.com/blog/how-to-get-slack-webhook-url/features/transform-webhooks/) into a Slack message, and forward it to your Slack URL — optionally [fanning out](https://webhookrelay.com/blog/how-to-get-slack-webhook-url/features/webhook-multiple-destinations/) to other destinations too. - [Send a webhook to Slack](https://webhookrelay.com/blog/how-to-get-slack-webhook-url/blog/webhook-to-slack/) — the full forward-and-transform walkthrough. - [Slack webhook tester](https://webhookrelay.com/blog/how-to-get-slack-webhook-url/blog/slack-webhook-tester/) — inspect exactly what you're sending. - [Receive Slack events locally](https://webhookrelay.com/blog/how-to-get-slack-webhook-url/blog/receive-slack-events-locally/) — for the other direction (Slack → your app). ## [Security notes](#security-notes) - The URL is a **secret**. Anyone with it can post to your channel. - To **revoke** a webhook, delete it from the app's Incoming Webhooks page (or remove the app) and create a new one. - Incoming webhooks can only **post** to the chosen channel — they can't read messages or act as a user. Ready to route real events into Slack? [Test a webhook now](https://webhookrelay.com/blog/how-to-get-slack-webhook-url/webhook-bin/) or [start forwarding for free](https://my.webhookrelay.com/register). --- --- title: How to Get a Discord Webhook URL | WebhookRelay meta: "og: title": "How to Get a Discord Webhook URL" description: Create a Discord webhook URL step by step: Server Settings, Integrations, pick a channel, copy the URL. Includes the format, a curl example and embeds. url: https://webhookrelay.com/blog/how-to-get-discord-webhook-url.md file: /blog/how-to-get-discord-webhook-url.md --- ![Stripes](https://webhookrelay.com/blog/how-to-get-discord-webhook-url/images/stripes.svg) # **How to Get a Discord Webhook URL** Create a Discord webhook URL step by step: Server Settings, Integrations, pick a channel, copy the URL. Includes the format, a curl example and embeds. A **Discord webhook URL** is the fastest way to post messages into a Discord channel from any app or script — no bot, no gateway connection, just an HTTP request. Here's how to create one and use it. (New to webhooks? See [what is a webhook](https://webhookrelay.com/blog/how-to-get-discord-webhook-url/blog/what-is-webhook/).) ## [What a Discord webhook URL looks like](#what-a-discord-webhook-url-looks-like) ``` https://discord.com/api/webhooks/123456789012345678/aBcDeF_secret-token ``` It's a numeric **webhook ID** plus a secret **token**. Anyone with the full URL can post to the channel, so keep it private. ## [Create a Discord webhook](#create-a-discord-webhook) You'll need the **Manage Webhooks** permission on the server (server owners and admins have it): 1. **Open Server Settings.** Click your server name (top-left) → **Server Settings**. 2. **Go to Integrations.** In the sidebar, choose **Integrations**, then **Webhooks** → **New Webhook** (or **Create Webhook**). 3. **Configure it.** Give the webhook a **name**, optionally an avatar, and pick the **channel** it should post to from the dropdown. 4. **Copy the URL.** Click **Copy Webhook URL**. That's the URL you'll POST to. You can also create a webhook straight from a channel: **Edit Channel → Integrations → Webhooks**. ![Creating a webhook in Discord under Integrations → Webhooks — the New Webhook button, the webhook's name and channel, and the Copy Webhook URL button](https://webhookrelay.com/blog/how-to-get-discord-webhook-url/images/guides/discord-create-webhook.png) ## [Send a test message](#send-a-test-message) POST JSON with a `content` field: ``` curl -H "Content-Type: application/json" \ -d '{"content":"Hello from a Discord webhook 👋"}' \ "https://discord.com/api/webhooks/123456789012345678/aBcDeF_secret-token" ``` For a richer message, send an **embed** — a titled, colored card: ``` curl -H "Content-Type: application/json" -d '{ "embeds": [{ "title": "Deploy finished", "description": "Build #142 shipped to production", "color": 3066993 }] }' "https://discord.com/api/webhooks/123456789012345678/aBcDeF_secret-token" ``` You can override the display name and avatar per message with `username` and `avatar_url`. ## [Sending _any_ event to Discord](#sending-any-event-to-discord) The webhook URL is the easy half — you still need to POST to it in Discord's format. Webhook Relay bridges that gap: point a provider (a payment, a CI build, a [TradingView alert](https://webhookrelay.com/blog/how-to-get-discord-webhook-url/blog/trading-view/)) at Webhook Relay, [transform the payload](https://webhookrelay.com/blog/how-to-get-discord-webhook-url/features/transform-webhooks/) into a Discord message or embed, and forward it — optionally [fanning out](https://webhookrelay.com/blog/how-to-get-discord-webhook-url/features/webhook-multiple-destinations/) to Slack or Teams at the same time. - [Send a webhook to Discord](https://webhookrelay.com/blog/how-to-get-discord-webhook-url/blog/webhook-to-discord/) — the full forward-and-transform walkthrough. - [TradingView webhooks to Discord](https://webhookrelay.com/blog/how-to-get-discord-webhook-url/blog/trading-view/) — a real automation example. ## [Security notes](#security-notes) - The URL is a **secret** — Discord even warns that leaked webhook URLs are a common way servers get spammed. - To **revoke** one, open the webhook in **Integrations → Webhooks** and click **Delete Webhook**; create a new one if needed. - A webhook can only **post** to its channel — it can't read messages or act as a member. Ready to route real events into Discord? [Test a webhook now](https://webhookrelay.com/blog/how-to-get-discord-webhook-url/webhook-bin/) or [start forwarding for free](https://my.webhookrelay.com/register). --- --- title: How to Create a Microsoft Teams Webhook (Workflows, 2026) meta: "og: title": "How to Create a Microsoft Teams Webhook (Workflows, 2026)" description: Microsoft retired Office 365 Connectors. Here's the current way to create a Teams incoming webhook with Workflows (Power Automate), step by step. url: https://webhookrelay.com/blog/microsoft-teams-webhook.md file: /blog/microsoft-teams-webhook.md --- ![Stripes](https://webhookrelay.com/blog/microsoft-teams-webhook/images/stripes.svg) # **How to Create a Microsoft Teams Webhook (Workflows, 2026)** Microsoft retired Office 365 Connectors. Here's the current way to create a Teams incoming webhook with Workflows (Power Automate), step by step. If you followed an older tutorial that said _"open the channel's Connectors and add an Incoming Webhook,"_ it may no longer work: **Microsoft is retiring Office 365 / Microsoft 365 Connectors** in Teams. The current, supported way to get an incoming webhook is **Workflows** (Power Automate). This guide walks the Workflows flow end to end. (New to webhooks? See [what is a webhook](https://webhookrelay.com/blog/microsoft-teams-webhook/blog/what-is-webhook/).) ## [Why the old connector method is going away](#why-the-old-connector-method-is-going-away) The classic path — channel **••• → Connectors → Incoming Webhook → Configure** — relied on Office 365 Connectors, which Microsoft has announced it's **retiring**. Existing connector webhooks are being phased out and new ones are discouraged. Microsoft's replacement is a **Workflows** template that does the same job: accept an HTTP POST and post the result into a channel. (Timelines have shifted more than once — verify the current status for your tenant.) ## [Create a Teams webhook with Workflows](#create-a-teams-webhook-with-workflows) 1. **Open the channel menu.** Select **More options (•••)** next to the channel (or chat) you want to post into. 2. **Choose Workflows.** Click **Workflows**. 3. **Pick the webhook template.** Select **"Post to a channel when a webhook request is received"** (there's a chat variant too; it may not be available on every organization type). 4. **Name it and sign in.** Give the workflow a name and authenticate with your Microsoft account (switch accounts if needed), then **Next**. 5. **Choose the destination.** Select the **Team** and **Channel** the messages should land in, then **Add workflow**. 6. **Copy the URL.** A dialog shows the generated **HTTP POST URL** — copy it. That's your incoming webhook. You can also build it from scratch in the **Workflows** app: **Create → Create from blank**, add the trigger **"When a Teams webhook request is received,"** then an action like **Post card in a chat or channel**, and copy the trigger's URL. ## [The URL and payload](#the-url-and-payload) The URL is a long Power Automate endpoint, roughly: ``` https://prod-00.westus.logic.azure.com:443/workflows/abc.../triggers/manual/paths/invoke?api-version=2016-06-01&sp=...&sig=SECRET ``` The `sig` parameter is a secret — **treat the whole URL as sensitive**. The default template posts an **Adaptive Card**, so send a JSON body shaped like a card attachment: ``` { "type": "message", "attachments": [ { "contentType": "application/vnd.microsoft.card.adaptive", "content": { "type": "AdaptiveCard", "version": "1.4", "body": [ { "type": "TextBlock", "text": "Deploy finished: build #142 is live", "wrap": true } ] } } ] } ``` The exact shape depends on how the flow's **Post card** action is configured, so open your workflow to confirm what its trigger expects. ## [Sending _any_ event to Teams](#sending-any-event-to-teams) Getting the URL is the first half; you still need to POST an Adaptive Card in the right shape whenever something happens. Webhook Relay handles that: point a provider at Webhook Relay, [transform the payload](https://webhookrelay.com/blog/microsoft-teams-webhook/features/transform-webhooks/) into a Teams card, and forward it to your Workflows URL — [fanning out](https://webhookrelay.com/blog/microsoft-teams-webhook/features/webhook-multiple-destinations/) to Slack or Discord at the same time if you like. See [send a webhook to Microsoft Teams](https://webhookrelay.com/blog/microsoft-teams-webhook/blog/webhook-to-microsoft-teams/). Ready to route real events into Teams? [Test a webhook now](https://webhookrelay.com/blog/microsoft-teams-webhook/webhook-bin/) or [start forwarding for free](https://my.webhookrelay.com/register). --- --- title: How to Send a Webhook from Google Forms (Apps Script) meta: "og: title": "How to Send a Webhook from Google Forms (Apps Script)" description: Google Forms has no built-in webhooks, but a few lines of Apps Script and an on-form-submit trigger will POST every response to your URL. Copy-paste code. url: https://webhookrelay.com/blog/google-forms-webhook.md file: /blog/google-forms-webhook.md --- ![Stripes](https://webhookrelay.com/blog/google-forms-webhook/images/stripes.svg) # **How to Send a Webhook from Google Forms (Apps Script)** Google Forms has no built-in webhooks, but a few lines of Apps Script and an on-form-submit trigger will POST every response to your URL. Copy-paste code. Google Forms is great at collecting responses but has **no native webhooks** — there's no field where you paste a URL. The fix is a few lines of **Google Apps Script** that POST each response to your endpoint. This guide shows the correct, minimal setup, and avoids a step you'll see in other tutorials that you don't actually need. (New to webhooks? See [what is a webhook](https://webhookrelay.com/blog/google-forms-webhook/blog/what-is-webhook/).) ## [The approach](#the-approach) Every Google Form can have a bound Apps Script. We'll: 1. Add a function that turns a submitted response into JSON and POSTs it with `UrlFetchApp`. 2. Wire it to an **installable "On form submit" trigger** so it runs on every submission. That's it — **no "Deploy as web app"** step. (Deploying as a web app is for _receiving_ HTTP requests, not for sending one on submit; adding it here just exposes an endpoint you don't need.) ## [Step 1 — open the script editor](#step-1-open-the-script-editor) In the form editor, click the **⋮ (three dots)** menu (top-right, near Send) → **Apps Script**. This opens a script bound to your form. ## [Step 2 — add the code](#step-2-add-the-code) Replace the default contents with this, and set `ENDPOINT_URL` to your webhook URL: ``` const ENDPOINT_URL = 'https://your-endpoint.example.com/webhook'; function onFormSubmit(e) { const payload = {}; // e.response is the FormResponse for THIS submission e.response.getItemResponses().forEach(function (item) { payload[item.getItem().getTitle()] = item.getResponse(); }); payload.submittedAt = e.response.getTimestamp(); UrlFetchApp.fetch(ENDPOINT_URL, { method: 'post', contentType: 'application/json', payload: JSON.stringify(payload), muteHttpExceptions: true, }); } ``` Using the **event object `e.response`** is more reliable than fetching "the latest response" from the form — it's exactly the submission that fired the trigger, with no race conditions. ## [Step 3 — add an installable trigger](#step-3-add-an-installable-trigger) This is the step that trips people up. Because `UrlFetchApp` needs authorization, a _simple_ trigger won't work — you need an **installable** one: 1. In the Apps Script editor, click the **Triggers** icon (the alarm clock) in the left sidebar. 2. **Add Trigger**. 3. Choose function **`onFormSubmit`**, event source **From form**, event type **On form submit**. 4. **Save**, then **authorize** the script when Google prompts (you'll approve the permissions once). ![The Apps Script Add Trigger dialog set to function onFormSubmit, event source "From form" and event type "On form submit"](https://webhookrelay.com/blog/google-forms-webhook/images/guides/google-forms-apps-script-trigger.png) ## [Step 4 — test it](#step-4-test-it) First, capture what the form sends by pointing `ENDPOINT_URL` at a free [Webhook Bin](https://webhookrelay.com/blog/google-forms-webhook/webhook-bin/) — submit a test response and you'll see the exact JSON arrive. Then switch `ENDPOINT_URL` to your real handler. ## [Where the webhook goes next](#where-the-webhook-goes-next) Once your form responses are flowing as webhooks, Webhook Relay can route them anywhere: [transform](https://webhookrelay.com/blog/google-forms-webhook/features/transform-webhooks/) each submission and [fan it out](https://webhookrelay.com/blog/google-forms-webhook/features/webhook-multiple-destinations/) to multiple destinations, or deliver to a service on your own machine. - [Webhook to Google Sheets](https://webhookrelay.com/blog/google-forms-webhook/blog/webhook-to-google-sheets/) — log every submission to a sheet. - [Webhook to Slack](https://webhookrelay.com/blog/google-forms-webhook/blog/webhook-to-slack/) or [Discord](https://webhookrelay.com/blog/google-forms-webhook/blog/webhook-to-discord/) — get notified on each response. - [Receive it on localhost](https://webhookrelay.com/blog/google-forms-webhook/webhooks/) — drive a local handler with real submissions. Ready to turn form submissions into webhooks? [Grab a free Webhook Bin](https://webhookrelay.com/blog/google-forms-webhook/webhook-bin/) to see your first one, or [start forwarding for free](https://my.webhookrelay.com/register). --- --- title: Webhook Architecture Diagram: How Webhooks Flow End to End meta: "og: title": "Webhook Architecture Diagram: How Webhooks Flow End to End" description: A clear webhook architecture diagram — from the sender that emits an event to the endpoint that receives it, plus queues, retries and signature checks. url: https://webhookrelay.com/blog/webhook-architecture-diagram.md file: /blog/webhook-architecture-diagram.md --- ![Stripes](https://webhookrelay.com/blog/webhook-architecture-diagram/images/stripes.svg) # **Webhook Architecture Diagram: How Webhooks Flow End to End** A clear webhook architecture diagram — from the sender that emits an event to the endpoint that receives it, plus queues, retries and signature checks. Webhooks are simple in principle — one server POSTs to another when something happens — but a reliable webhook system has a few more moving parts than people expect. This is the **webhook architecture**, from the event that starts it to the handler that finishes it. (New to the concept? Start with [what is a webhook](https://webhookrelay.com/blog/webhook-architecture-diagram/blog/what-is-webhook/).) ## [The basic webhook architecture](#the-basic-webhook-architecture) At its core, every webhook flows the same way: ``` ┌────────────┐ event happens ┌──────────────┐ HTTP POST ┌──────────────┐ │ Sender │ ────────────────► │ Webhook │ ─────────────► │ Your public │ │ (provider) │ │ (HTTP POST) │ │ endpoint │ └────────────┘ └──────────────┘ 200 OK ◄──── └──────┬───────┘ │ enqueue ▼ ┌──────────────┐ │ Your queue │ │ + workers │ └──────────────┘ ``` 1. **The sender** (Stripe, GitHub, Twilio…) detects an event. 2. It makes an **HTTP POST** to the URL you registered, carrying headers (content type, an event-type header, a signature) and a JSON body. 3. **Your endpoint** receives it, returns a fast **2xx** to acknowledge, and hands the payload to a **queue** so a worker can process it without holding up the response. ### [The components](#the-components) | Component | Role | | --- | --- | | **Sender / producer** | The provider that emits the event and POSTs the webhook | | **HTTP request** | Method + URL, headers (incl. the signature), and the JSON payload | | **Receiver endpoint** | The public URL that accepts the POST and returns `2xx` fast | | **Signature verification** | Confirms the request is authentic (HMAC) before you trust it | | **Queue + workers** | Absorbs bursts and processes events asynchronously | | **Retry logic** | Re-delivers events your endpoint couldn't accept | ## [Production-grade webhook architecture](#production-grade-webhook-architecture) The basic flow works until real traffic hits it — a deploy drops events, a spike overwhelms a fragile service, a forged request slips through, or you need the same event in three systems. A production webhook architecture puts a **[webhook gateway](https://webhookrelay.com/blog/webhook-architecture-diagram/webhook-gateway/)** between the sender and your services to handle exactly that: - **[Signature verification](https://webhookrelay.com/blog/webhook-architecture-diagram/blog/webhook-authentication/)** so only genuine events are processed. - **[Durable retries](https://webhookrelay.com/blog/webhook-architecture-diagram/features/durable-retries/)** so an event survives an outage or a deploy — persisted and retried for up to 30 days. - **[Throttling](https://webhookrelay.com/blog/webhook-architecture-diagram/features/throttling/)** and a queue so a burst becomes a steady, survivable stream. - **[Fan-out to multiple destinations](https://webhookrelay.com/blog/webhook-architecture-diagram/features/webhook-multiple-destinations/)** so one event reaches your API, a backup and an analytics sink at once. - **[Delivery logs](https://webhookrelay.com/blog/webhook-architecture-diagram/features/webhook-logs/)** so you can see, inspect and replay every delivery. ![A production webhook architecture with Webhook Relay as the gateway](https://webhookrelay.com/blog/webhook-architecture-diagram/images/webhookrelay_basic_diagram.png) ## [Webhook architecture for local development](#webhook-architecture-for-local-development) There's one more architecture worth drawing: **receiving webhooks during development**. Providers can only POST to public URLs, but your handler runs on `localhost` or inside a private network. A reverse tunnel closes the gap: ``` Provider ──POST──► Webhook Relay (stable public URL) ──► relay agent (outbound) ──► localhost:3000 ``` The [relay agent](https://webhookrelay.com/blog/webhook-architecture-diagram/webhooks/) connects **outbound**, so events reach your machine with no public IP and no open firewall ports — and the URL stays fixed so you configure the provider once. See [how to test webhooks](https://webhookrelay.com/blog/webhook-architecture-diagram/blog/how-to-test-webhooks/). ## [Build the diagram into something real](#build-the-diagram-into-something-real) - [What is a webhook](https://webhookrelay.com/blog/webhook-architecture-diagram/blog/what-is-webhook/) — the anatomy of a single request. - [Webhooks vs API](https://webhookrelay.com/blog/webhook-architecture-diagram/blog/webhooks-vs-api/) and [webhook vs WebSocket](https://webhookrelay.com/blog/webhook-architecture-diagram/blog/webhook-vs-websocket/) — where webhooks fit. - [Webhook security best practices](https://webhookrelay.com/blog/webhook-architecture-diagram/blog/webhook-security/) — verification, secrets and idempotency. Want to watch a real request flow through this architecture? [Open a free Webhook Bin](https://webhookrelay.com/blog/webhook-architecture-diagram/webhook-bin/) and send it a webhook, or [start forwarding to localhost for free](https://my.webhookrelay.com/register). --- --- title: Webhooks in CI/CD: GitHub Actions & Jenkins | WebhookRelay meta: "og: title": "Webhooks in CI/CD: GitHub Actions & Jenkins" description: CI/CD runners are ephemeral and private, so providers can't reach them. How to receive webhooks in GitHub Actions, Jenkins and Harness behind a firewall. url: https://webhookrelay.com/blog/webhooks-in-ci-cd.md file: /blog/webhooks-in-ci-cd.md --- ![Stripes](https://webhookrelay.com/blog/webhooks-in-ci-cd/images/stripes.svg) # **Receiving Webhooks in CI/CD: GitHub Actions, Jenkins & Harness (2026)** CI/CD runners are ephemeral and private, so providers can't reach them. How to receive webhooks in GitHub Actions, Jenkins and Harness behind a firewall. ![Receiving webhooks in CI/CD: GitHub Actions, Jenkins and Harness](https://webhookrelay.com/blog/webhooks-in-ci-cd/images/blog/heroes/route.jpg) Continuous integration and delivery runs on webhooks — a push, a merged PR, a released tag, a provider event kicks off a build or a deploy. But CI/CD infrastructure is the _worst_ place to receive a webhook: runners are **ephemeral and usually private**, so the sender has nothing stable to point at, and anything that arrives while a runner is spinning up is simply gone. This is the same problem people hit with [ngrok](https://webhookrelay.com/blog/webhooks-in-ci-cd/blog/ngrok-alternative/), amplified. The fix is the same shape: a **persistent URL** the provider is configured with once, plus an **outbound agent** that delivers into your private network — so no runner ever needs a public IP. ## [The problem: CI/CD runners have no stable public address](#the-problem-cicd-runners-have-no-stable-public-address) Three things make webhooks hard in CI/CD: - **No public IP.** Self-hosted GitHub Actions runners, on-prem Jenkins controllers and self-managed Harness delegates all sit behind a firewall. Opening inbound ports to them is exactly what your security team doesn't want. - **Ephemerality.** A fresh runner comes up with a new address and disappears when the job ends. A URL you paste into a provider today is meaningless tomorrow. - **Timing.** The webhook that triggers a job often fires _before_ the environment is ready. Miss it and the pipeline silently never runs. ## [The pattern: a persistent bucket + the relay agent](#the-pattern-a-persistent-bucket-the-relay-agent) Webhook Relay gives you a **bucket** with a URL that never changes. You configure the provider (GitHub, GitLab, Bitbucket, Stripe, a partner API) with that URL **once**. Inside your network, the lightweight [relay agent](https://webhookrelay.com/blog/webhooks-in-ci-cd/features/webhook-to-internal-server/) connects _outbound_ to Webhook Relay and forwards each event to wherever it needs to land — a runner, a controller, an internal deploy hook. No inbound ports, no public IP, and the provider config survives every runner that comes and goes. Because it's a full [webhook gateway](https://webhookrelay.com/blog/webhooks-in-ci-cd/webhook-gateway/), you also get signature verification, [payload transformation](https://webhookrelay.com/blog/webhooks-in-ci-cd/features/transform-webhooks/), [fan-out](https://webhookrelay.com/blog/webhooks-in-ci-cd/features/webhook-multiple-destinations/) and [durable retries](https://webhookrelay.com/blog/webhooks-in-ci-cd/features/durable-retries/) on top of the tunnel. ## [GitHub Actions](#github-actions) There are two distinct jobs people mean by "webhooks in GitHub Actions": **1. Trigger a workflow from an external event.** GitHub starts workflows from its own `repository_dispatch` / `workflow_dispatch` API. Point a provider at a Webhook Relay bucket, use a [transformation](https://webhookrelay.com/blog/webhooks-in-ci-cd/features/transform-webhooks/) to reshape the incoming payload into a `repository_dispatch` call, and forward it to GitHub — a clean way to kick off a pipeline from a system that doesn't speak GitHub's API natively. **2. Receive a webhook on a self-hosted runner.** When a job needs to _receive_ an external callback — a deploy approval, a test event from a third-party sandbox, a partner's webhook — run the relay agent so the event is delivered to the runner's local endpoint over the outbound tunnel. A shared bucket means the same event can reach every runner in a matrix at once. ## [Jenkins](#jenkins) Jenkins is the classic case: GitHub, GitLab or Bitbucket needs to notify Jenkins to start a build, but Jenkins lives on-prem or inside a cluster. Point the provider at a persistent bucket URL and run the agent next to Jenkins: ``` relay forward --bucket acme-ci http://localhost:8080/github-webhook/ ``` The agent connects outbound, so there are no firewall changes. We've documented the common setups in depth: - [GitHub → Jenkins: the complete webhook guide](https://webhookrelay.com/blog/webhooks-in-ci-cd/blog/github-jenkins-guide/) - [Webhooks to Jenkins running on Kubernetes](https://webhookrelay.com/blog/webhooks-in-ci-cd/blog/webhooks-to-jenkins-on-kubernetes/) - [Automated GitHub pull-request builds on Jenkins](https://webhookrelay.com/blog/webhooks-in-ci-cd/blog/automated-github-pull-request-builds-on-jenkins/) ## [Harness](#harness) Harness triggers pipelines from webhooks too, and its self-managed delegates run inside your network. Run the relay agent alongside the delegate (or the internal endpoint you use for custom webhook triggers) and Webhook Relay delivers inbound trigger webhooks to it over an outbound connection — no public exposure of your Harness install. As of 2026, check Harness's current webhook-trigger and delegate configuration and point the agent at the endpoint it expects. ## [Share one endpoint across the team and the pipeline](#share-one-endpoint-across-the-team-and-the-pipeline) The broadcast model that makes Webhook Relay useful for teams is just as useful across a pipeline. **Multiple agents can connect to the same bucket, and each one receives every webhook** — so a single provider event can reach a developer's laptop, a staging deploy hook and a CI runner at the same time, from one URL you configured once. It's the piece a plain tunnel can't do; see the [ngrok alternative writeup](https://webhookrelay.com/blog/webhooks-in-ci-cd/blog/ngrok-alternative/#2-one-bucket-your-whole-team) for the full pattern. ## [Don't lose events while a runner spins up](#dont-lose-events-while-a-runner-spins-up) The webhook that starts a pipeline often arrives before the environment is ready. With [durable retries](https://webhookrelay.com/blog/webhooks-in-ci-cd/features/durable-retries/) the event is persisted the moment it reaches Webhook Relay and retried — with exponential backoff, for up to 30 days — so a webhook that fired while a runner was booting or a controller was restarting still gets delivered instead of vanishing. ## [Get started](#get-started) 1. [Create a free account](https://my.webhookrelay.com/register) and create a bucket — that's your persistent URL. 2. Point your provider (GitHub, GitLab, Bitbucket, Harness…) at the bucket URL. 3. Run `relay forward --bucket ` next to your runner, Jenkins controller or delegate. 4. Optionally add a [transformation](https://webhookrelay.com/blog/webhooks-in-ci-cd/features/transform-webhooks/) or [fan-out](https://webhookrelay.com/blog/webhooks-in-ci-cd/features/webhook-multiple-destinations/) to route the same event to more than one target. Want to inspect what a provider is actually sending first? Open a free [Webhook Bin](https://webhookrelay.com/blog/webhooks-in-ci-cd/webhook-bin/) and watch the requests live before you wire anything up. --- --- title: Secure webhooks to Jenkins on Kubernetes | WebhookRelay meta: "og: title": "Secure webhooks to Jenkins on Kubernetes" description: A tutorial on how to securely receive GitHub webhooks on your Jenkins inside a Kubernetes cluster url: https://webhookrelay.com/blog/webhooks-to-jenkins-on-kubernetes.md file: /blog/webhooks-to-jenkins-on-kubernetes.md --- ![Stripes](https://webhookrelay.com/blog/webhooks-to-jenkins-on-kubernetes/images/stripes.svg) # **Secure webhooks to Jenkins on Kubernetes** A tutorial on how to securely receive GitHub webhooks on your Jenkins inside a Kubernetes cluster ![satellite dish picture](https://webhookrelay.com/blog/webhooks-to-jenkins-on-kubernetes/images/blog/jenkins-on-kubernetes/cover.jpg)_Photo by [Marat Gilyadzinov](https://unsplash.com/@m3design)_ ## [Problem](#problem) There is no doubt that Jenkins is a great tool for both CI & CD. However, due to its access to your infrastructure, it becomes an easy target for attackers. For this reason Jenkins is often put behind a firewall and in doing so, webhooks stop working. Users do not want the pull-based but rather prefer the build to start as soon as there is a commit/tag/docker push! ## [Solution](#solution) ![webhooks to Jenkins on Kubernetes](https://webhookrelay.com/blog/webhooks-to-jenkins-on-kubernetes/images/blog/jenkins-on-kubernetes/webhooks-jenkins-kubernetes.png) Webhook Relay allows webhooks to start working again in a secure way, i.e. traffic is allowed to go only one way. Main advantages of using Webhook Relay: - Security for your Jenkins instance by allowing only one-way traffic. - Your Jenkins instance doesn't have to be exposed to the internet. It can even be running on your local machine without configuring NAT/firewall. - Auditability (webhook logs can be reviewed). - Resend webhooks via Webhook Relay dashboard to make testing or adding new integrations easier. ## [Which providers can work with this approach?](#which-providers-can-work-with-this-approach) Currently, there is no limitation on which Git (or anything else) can work with this approach so if you are using Github, Gitlab, you will be a perfect candidate. The only limitation at the moment is the webhook size - 3MB. However, this should be sufficient as standard Github webhooks are 8KB size. ## [Prerequisites](#prerequisites) - [Webhook Relay account](https://my.webhookrelay.com) - Kubernetes environment, we will be using [Minikube](https://github.com/kubernetes/minikube) - Github account - Some knowledge about [Jenkins](https://jenkins.io/) > You can use any Kubernetes environment, just one step might be different (getting Jenkins authentication token). Github can be exchanged for Gitlab, as Jenkins supports those webhooks too. ## [Deployment](#deployment) Our strategy is quite simple. At first, we create a secret with authentication details to Jenkins. We then deploy a Jenkins instance with Webhook Relay as a sidecar. Once it's deployed, we sign in into Jenkins and create a freestyle project. ### [Preparing Webhook Relay token and secret](#preparing-webhook-relay-token-and-secret) Go to your [tokens page](https://my.webhookrelay.com/tokens) and create a token key & secret. Once this is done, create a Kubernetes secret ``` kubectl --namespace default create \\ secret generic webhookrelay-credentials \\ --from-literal=key=[TOKEN KEY] \\ --from-literal=secret=[TOKEN SECRET] ``` This secret will be used by our Webhook Relay sidecar to authenticate to your account and receive your webhooks. ### [Configuring GitHub webhooks](#configuring-github-webhooks) First, let's create a Webhook Relay bucket to get our public endpoint: 1. Go to [https://my.webhookrelay.com/buckets](https://my.webhookrelay.com/buckets) 2. Click on **CREATE BUCKET** in the top right corner 3. Name bucket 'jenkins' and add sidecar configuration: ![Create bucket](https://webhookrelay.com/blog/webhooks-to-jenkins-on-kubernetes/images/blog/jenkins-on-kubernetes/jenkins-sidecar.png) Since our agent will be running as a sidecar, bucket output should be **internal** and destination should be set to `http://localhost:8080/github-webhook/`. Copy your input public URL (the one that starts with `https://my.webhookrelay.com/v1/webhooks/....`) as you will need it in the next step. 1. Go to your GitHub repository settings, then click on "Webhooks" and add your Webhook Relay public URL: ![Add GitHub webhook](https://webhookrelay.com/blog/webhooks-to-jenkins-on-kubernetes/images/blog/jenkins-on-kubernetes/configure-webhook.png) Content type should be JSON. ### [(Optional) Customizing Jenkins Docker image](#optional-customizing-jenkins-docker-image) In this tutorial we are using a slightly customized Jenkins image that has [Golang](https://golang.org/). There's no reason to add it if you are not using Go. Our _Dockerfile_: ``` FROM jenkins/jenkins:latest EXPOSE 8080 50000 USER root # RUN add-apt-repository ppa:duh/golang RUN apt-get update && apt-get install -y golang ENTRYPOINT ["/sbin/tini", "--", "/usr/local/bin/jenkins.sh"] ``` If you want to build your own image, just add any necessary steps and run: ``` docker build -t /jenkins-ci:latest -f Dockerfile . docker push /jenkins-ci:latest ``` ### [Create Jenkins deployment](#create-jenkins-deployment) Now, it's time to deploy our Jenkins instance. Save this file and use `kubectl` to deploy. ``` # deployment.yaml apiVersion: apps/v1beta2 kind: Deployment metadata: name: jenkins-ci namespace: default spec: replicas: 1 selector: matchLabels: app: jenkins-ci name: jenkins-ci template: metadata: labels: app: jenkins-ci name: jenkins-ci spec: containers: - name: jenkins-ci imagePullPolicy: Always image: karolisr/jenkins-ci:latest ports: - containerPort: 8080 - containerPort: 50000 readinessProbe: tcpSocket: port: 8080 initialDelaySeconds: 40 periodSeconds: 20 securityContext: privileged: true volumeMounts: - mountPath: /var/run name: docker-sock - mountPath: /var/jenkins_home name: jenkins-home resources: limits: cpu: 300m memory: 512Mi requests: cpu: 150m memory: 256Mi - name: webhookrelayd image: "webhookrelay/webhookrelayd:latest" imagePullPolicy: IfNotPresent command: ["/relayd"] env: - name: KEY valueFrom: secretKeyRef: name: webhookrelay-credentials key: key - name: SECRET valueFrom: secretKeyRef: name: webhookrelay-credentials key: secret - name: BUCKET value: "jenkins" resources: limits: cpu: 100m memory: 128Mi requests: cpu: 50m memory: 64Mi volumes: - name: docker-sock hostPath: path: /var/run - name: jenkins-home hostPath: path: /var/jenkins_home --- apiVersion: v1 kind: Service metadata: name: jenkins-ci-lb spec: type: LoadBalancer ports: - name: jenkins port: 8080 targetPort: 8080 - name: jenkins-agent port: 50000 targetPort: 50000 selector: name: jenkins-ci ``` ``` kubectl create -f deployment.yaml ``` You should see something like this: ``` $ kubectl apply -f deployment.yaml deployment.apps/jenkins-ci created service/jenkins-ci-lb created $ kubectl get pods NAME READY STATUS RESTARTS AGE jenkins-ci-975f88b66-pjqjf 0/2 ContainerCreating 0 3 ``` ### [Connecting to your Jenkins](#connecting-to-your-jenkins) Since we are using minikube: ``` kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE jenkins-ci-lb LoadBalancer 10.97.25.217 8080:30584/TCP,50000:30214/TCP 4m40s kubernetes ClusterIP 10.96.0.1 443/TCP 18m ``` ``` echo $(minikube service jenkins-ci-lb --url) http://192.168.99.100:30584 http://192.168.99.100:30214 ``` Now, you should see Jenkins login screen. Let's retrieve the password: ``` # token will be on the VM where the Jenkins is running minikube ssh # once logged in, get the admin token: sudo cat /var/jenkins_home/secrets/initialAdminPassword 867888abd88c43f49504db3dc11b64b3 ``` ### [Create a job](#create-a-job) Let's choose 'Freestyle Project': ![Jenkins freestyle project](https://webhookrelay.com/blog/webhooks-to-jenkins-on-kubernetes/images/blog/jenkins-on-kubernetes/free-style-project.png) After creating a new job, go to **Source Code Management**. Now, set **Repository URL** with your GitHub repository address and tick **GitHub hook trigger for GITScm polling**. The idea here is that once any push event happens, GitHub will send a webhook and your Jenkins will clone the repository to do the build: ![Create Jenkins build job](https://webhookrelay.com/blog/webhooks-to-jenkins-on-kubernetes/images/blog/jenkins-on-kubernetes/jenkins-job-create.png) In the build step we will just add a simple 'Execute shell' step: ``` go test ``` Save the job. ### [Push to repository and observe](#push-to-repository-and-observe) Now, whenever we push to GitHub, a webhook will be sent through Webhook Relay sidecar to the Jenkins in a secure way. Your Jenkins instance will not be exposed to the internet; only one path at `http://localhost:8080/github-webhook/` will be able to accept HTTP requests from outside. If you go to your Webhook Relay `jenkins` bucket details, you should see a new request being delivered: ![webhook logs](https://webhookrelay.com/blog/webhooks-to-jenkins-on-kubernetes/images/blog/jenkins-on-kubernetes/bucket-logs.png) And our Jenkins job result: ``` Started by GitHub push by rusenask Building in workspace /var/jenkins_home/workspace/k8s-webhooks-demo > git rev-parse --is-inside-work-tree # timeout=10 Fetching changes from the remote Git repository > git config remote.origin.url https://github.com/webhookrelay/jenkins-kubernetes # timeout=10 Fetching upstream changes from https://github.com/webhookrelay/jenkins-kubernetes > git --version # timeout=10 > git fetch --tags --progress https://github.com/webhookrelay/jenkins-kubernetes +refs/heads/*:refs/remotes/origin/* > git rev-parse refs/remotes/origin/master^{commit} # timeout=10 > git rev-parse refs/remotes/origin/origin/master^{commit} # timeout=10 Checking out Revision f32e8ceee2f03163dac2532dd82f0db6a84147d5 (refs/remotes/origin/master) > git config core.sparsecheckout # timeout=10 > git checkout -f f32e8ceee2f03163dac2532dd82f0db6a84147d5 Commit message: "updated agent version" > git rev-list --no-walk cbc7fcbcda120bf39cf346f8c4d5345b87e402cb # timeout=10 [k8s-webhooks-demo] $ /bin/sh -xe /tmp/jenkins9068732710526083316.sh + go test PASS ok _/var/jenkins_home/workspace/k8s-webhooks-demo 0.001s Finished: SUCCESS ``` ## [Going further](#going-further) Once the configuration is in place, you can set the same Webhook Relay input endpoint URL to multiple GitHub repositories. Jenkins GitHub plugin will start correct jobs based on webhook contents. P.S. If you are not using Kubernetes, check out my other blog post about [receiving webhooks on Jenkins without public IP](https://webhookrelay.com/blog/webhooks-to-jenkins-on-kubernetes/blog/2017/11/23/github-jenkins-guide/). --- --- title: Twilio Webhooks: Setup, Payloads & Signatures | WebhookRelay meta: "og: title": "Twilio Webhooks: Setup, Payloads & Signatures" description: A complete guide to Twilio webhooks: configuring SMS and voice, the form-encoded payload, validating X-Twilio-Signature, retries, and local testing. url: https://webhookrelay.com/blog/twilio-webhooks-guide.md file: /blog/twilio-webhooks-guide.md --- ![Stripes](https://webhookrelay.com/blog/twilio-webhooks-guide/images/stripes.svg) # **Twilio Webhooks: Setup, Payloads, Signature Validation & Testing** A complete guide to Twilio webhooks: configuring SMS and voice, the form-encoded payload, validating X-Twilio-Signature, retries, and local testing. Twilio uses **webhooks** for almost everything reactive: an incoming SMS or MMS, an inbound call, and status updates as your messages and calls move through the network. This guide covers how Twilio webhooks work, how to configure and secure them, and how to test them without deploying. (New to webhooks generally? Start with [what is a webhook](https://webhookrelay.com/blog/twilio-webhooks-guide/blog/what-is-webhook/).) ## [How Twilio webhooks work](#how-twilio-webhooks-work) When something happens, Twilio makes an **HTTP request to a URL you control**. By default it's a `POST`, and — importantly — the body is **`application/x-www-form-urlencoded`**, not JSON. Twilio expects a quick response: **TwiML** (XML) for messaging and voice so it knows what to do next, or a plain `2xx` for status callbacks. There are three broad categories: - **Incoming messages** — someone texts your Twilio number; Twilio POSTs the message to your _Messaging_ webhook and uses your TwiML reply to respond. - **Incoming voice** — an inbound call hits your number; Twilio fetches TwiML from your _Voice_ webhook. - **Status callbacks** — as a message or call changes state (`queued` → `sent` → `delivered`/`failed`), Twilio POSTs updates to the `StatusCallback` URL you set. ## [Configuring a Twilio webhook](#configuring-a-twilio-webhook) You set webhook URLs in a few places depending on the product: 1. **Per phone number** — Console → **Phone Numbers → Manage → Active numbers** → your number → _Messaging_ / _Voice_ "A message/call comes in" webhook. 2. **Messaging Service** — Console → **Messaging → Services** → your service → _Integration_. 3. **Per API request** — pass a `StatusCallback` URL when you create a message or call to receive delivery status. ## [The Twilio webhook payload](#the-twilio-webhook-payload) Because it's form-encoded, you parse parameters, not JSON. A typical inbound SMS looks like: ``` POST /twilio HTTP/1.1 Content-Type: application/x-www-form-urlencoded X-Twilio-Signature: hZ8...= MessageSid=SM123&From=%2B14155551212&To=%2B14155557890&Body=Hello&NumMedia=0 ``` Common fields include `MessageSid`, `From`, `To`, `Body`, `NumMedia` (messaging) and `CallSid`, `CallStatus`, `MessageStatus` (voice / status callbacks). Parse the body with your framework's URL-encoded form parser — for example `express.urlencoded()` in Node or `request.form` in Flask. ### [Responding with TwiML](#responding-with-twiml) For messaging and voice, reply with TwiML telling Twilio what to do: ``` Thanks, we got your text! ``` For status callbacks there's nothing to instruct, so just return `204`/`200`. ## [Validating the X-Twilio-Signature](#validating-the-x-twilio-signature) Every Twilio request carries an **`X-Twilio-Signature`** header so you can confirm it really came from Twilio. It's an **HMAC-SHA1** of the **full request URL** plus the **alphabetically sorted POST parameters**, keyed with your **Auth Token**, then Base64-encoded. Recompute it and compare — most people use Twilio's `RequestValidator` helper. The classic gotcha: the URL you validate against must be the **exact public URL Twilio called** (scheme, host, path and query string). Behind a proxy or tunnel, the host your app sees may differ from the public host — validate against the public one. See [how to verify a webhook signature](https://webhookrelay.com/blog/twilio-webhooks-guide/blog/verify-webhook-signature/), try the [Twilio signature verifier](https://webhookrelay.com/blog/twilio-webhooks-guide/verify-twilio-webhook-signature/), or use the free [HMAC verifier](https://webhookrelay.com/blog/twilio-webhooks-guide/hmac-verification/). ## [Retries and reliability](#retries-and-reliability) Twilio's retry behavior varies by product and is generally limited — if your endpoint is slow or returns an error, you can lose the notification (verify Twilio's current behavior for your specific webhook type). For anything business-critical, put a [webhook gateway](https://webhookrelay.com/blog/twilio-webhooks-guide/webhook-gateway/) in front of your handler: it verifies the signature, persists each event and applies [durable retries](https://webhookrelay.com/blog/twilio-webhooks-guide/features/durable-retries/) so a deploy or a brief outage doesn't drop a delivery, and it keeps [delivery logs](https://webhookrelay.com/blog/twilio-webhooks-guide/features/webhook-logs/) you can inspect and replay. ## [Testing Twilio webhooks locally](#testing-twilio-webhooks-locally) You don't want to deploy just to see what Twilio sends. Two options: 1. **Inspect first (no install):** open a free [Webhook Bin](https://webhookrelay.com/blog/twilio-webhooks-guide/webhook-bin/), paste its URL into your Twilio number's webhook field, and text the number — you'll see the exact form-encoded request and headers. See the [Twilio webhook tester](https://webhookrelay.com/blog/twilio-webhooks-guide/blog/twilio-webhook-tester/). 2. **Forward to localhost:** point Twilio at a stable Webhook Relay URL and run the agent so requests reach your machine with no public IP: ``` relay forward --bucket twilio-dev http://localhost:3000/twilio ``` Full walkthrough: [receive Twilio webhooks on localhost](https://webhookrelay.com/blog/twilio-webhooks-guide/blog/receive-twilio-webhooks-locally/). ## [Common issues](#common-issues) - **Empty body / can't read fields** — you're parsing JSON; Twilio sends **form-encoded** data. Use a URL-encoded parser. - **Signature always fails** — the URL you validate doesn't match the public URL Twilio called (often a proxy/tunnel host or a missing query string). - **Twilio shows an 11200 error** — your endpoint didn't return a valid response (or valid TwiML) quickly enough; acknowledge fast and do heavy work asynchronously. ## [Next steps](#next-steps) - [Twilio webhook tester](https://webhookrelay.com/blog/twilio-webhooks-guide/blog/twilio-webhook-tester/) — inspect a real request in your browser. - [Receive Twilio webhooks on localhost](https://webhookrelay.com/blog/twilio-webhooks-guide/blog/receive-twilio-webhooks-locally/) — drive your local handler with real events. - [Webhook security best practices](https://webhookrelay.com/blog/twilio-webhooks-guide/blog/webhook-security/) — verification, idempotency and more. Ready to see your first Twilio webhook? [Open a free Webhook Bin](https://webhookrelay.com/blog/twilio-webhooks-guide/webhook-bin/) or [start forwarding to localhost for free](https://my.webhookrelay.com/register). --- --- title: Mailgun webhook fan-out | WebhookRelay meta: "og: title": "Mailgun webhook fan-out" description: How to send mailgun webhooks to multiple destinations url: https://webhookrelay.com/blog/mailgun-webhook-fanout.md file: /blog/mailgun-webhook-fanout.md --- ![Stripes](https://webhookrelay.com/blog/mailgun-webhook-fanout/images/stripes.svg) # **Mailgun webhook fan-out** How to send mailgun webhooks to multiple destinations Transactional emails are widely used in most apps. Systems requests email confirmation, dispatch password recovery emails, notifications, and more. Sending the emails is one thing, but what about tracking delivery and performance? Did that user get that email confirmation message? These unknowns could lead to unexpected problems. Even though this data is available via dashboard, webhooks offer a much better alternative where you just supply the URL and you get almost instant notifications about events. In this article we will explore how we can deliver webhook notifications to multiple systems at once, be it a development, testing or another production environment that expects webhooks. We will be using Mailgun provider while for catching webhooks we will use [http://bin.mailgun.net/](http://bin.mailgun.net/) and [https://bin.webhookrelay.com/](https://bin.webhookrelay.com/). ![webhook relay](https://webhookrelay.com/blog/mailgun-webhook-fanout/images/blog/mailgun-fanout/webhookrelay.gif) ## [Setup](#setup) Our setup will be: - Two or more "bins" that will be catching webhooks - Webhook Relay bucket with those two bins as destinations and a public input endpoint URL - Mailgun configuration that points to our Webhook Relay bucket's input endpoint URL ## [Creating bins](#creating-bins) You can create a new bin by visiting [http://bin.mailgun.net/](http://bin.mailgun.net/) address. Keep the browser tab open, you will need that unique bin address. ![mailgun bin](https://webhookrelay.com/blog/mailgun-webhook-fanout/images/blog/mailgun-fanout/mailgunbin.png) Now, head to [https://bin.webhookrelay.com/](https://bin.webhookrelay.com/), here you should also be redirected to your unique bin where you will be able to accept webhooks. ![webhook bin](https://webhookrelay.com/blog/mailgun-webhook-fanout/images/blog/mailgun-fanout/webhookbin.png) Both services will generate a unique endpoint that you can use to get a sample event data. In addition to storing request body data, Webhook Bin will also capture headers and can handle any request method (POST, PATCH, PUT...). ## [Configuring Webhook Relay bucket](#configuring-webhook-relay-bucket) If you already have an account with Webhook Relay (if not, please register [here](https://my.webhookrelay.com/register)), just proceed to [https://my.webhookrelay.com/buckets](https://my.webhookrelay.com/buckets) and click on "create bucket" button: ![bucket create](https://webhookrelay.com/blog/mailgun-webhook-fanout/images/blog/mailgun-fanout/buckets-create.png) Now, we will need to add our endpoints and get a unique URL for this bucket: 1. Add any name for the input, it's always good to give a name that is related to webhook producer 2. Add two outputs (leave 'internal' not ticked, it is only required for endpoints that are not publicly accessible and should be routed from **relay** agent) Your bucket configuration should look similar to this: ![bucket config](https://webhookrelay.com/blog/mailgun-webhook-fanout/images/blog/mailgun-fanout/bucket-config.png) ## [Setting up Mailgun webhooks](#setting-up-mailgun-webhooks) Once we have our Webhook Relay input endpoint and bucket configuration is complete, we can configure Mailgun webhooks. First, let's copy our input endpoint: ![input copy](https://webhookrelay.com/blog/mailgun-webhook-fanout/images/blog/mailgun-fanout/input-copy.png) Now, head to your app's webhook configuration, dashboard can be found here: [https://app.mailgun.com/app/webhooks](https://app.mailgun.com/app/webhooks) (you can also use [API](http://mailgun-documentation.readthedocs.io/en/latest/api-webhooks.html#webhooks)). The webhooks page lists the different event types you can receive event data for. By clicking the "+" icon in front of each event, you can set the URL where the event data will be sent. Click on "Delivered Messages" and add your bucket's endpoint URL: `https://my.webhookrelay.com/v1/webhooks/`, then click "Test Webhook": ![test webhook](https://webhookrelay.com/blog/mailgun-webhook-fanout/images/blog/mailgun-fanout/test-webhook.png) ## [Checking your Mailgun and Webhook bins](#checking-your-mailgun-and-webhook-bins) Mailgun webhook should have been routed through Webhook Relay bucket to those two external services (in our case those are just bins). Check your browser tabs that have those bins. They should look similarly to this (Mailgun bin will need a browser refresh to show changes) while Webhook Bin shows results immediately: ![captured mailgunbin](https://webhookrelay.com/blog/mailgun-webhook-fanout/images/blog/mailgun-fanout/captured-mailgunbin.png) And Webhook Bin: ![captured webhookbin](https://webhookrelay.com/blog/mailgun-webhook-fanout/images/blog/mailgun-fanout/captured-webhookbin.png) Webhook Bin sets request body prettifier based on content type header. You can select "RAW" from the dropdown menu to see the original message. Your Webhook Relay bucket details page should also show all request logs: ![bucket with logs](https://webhookrelay.com/blog/mailgun-webhook-fanout/images/blog/mailgun-fanout/bucket-with-logs.png) ## [Conclusion](#conclusion) Setting up Mailgun's webhooks is really easy and configuring Webhook Relay to forward all received webhooks to multiple destinations is trivial. You can easily forward a copy of webhooks to Slack or any other system before it reaches your backend service. If you want to read more on processing Mailgun's webhooks, they published an excellent article that you can find here: [http://blog.mailgun.com/a-guide-to-using-mailguns-webhooks/](http://blog.mailgun.com/a-guide-to-using-mailguns-webhooks/). --- --- title: Receive emails on new Stripe subscribers | WebhookRelay meta: "og: title": "Receive emails on new Stripe subscribers" description: It's nice to get Stripe notifications on new payments however we can turn any Stripe into an email url: https://webhookrelay.com/blog/stripe-webhook-to-email.md file: /blog/stripe-webhook-to-email.md --- ![Stripes](https://webhookrelay.com/blog/stripe-webhook-to-email/images/stripes.svg) # **Receive emails on new Stripe subscribers** It's nice to get Stripe notifications on new payments however we can turn any Stripe into an email ![Stripe to Email](https://webhookrelay.com/blog/stripe-webhook-to-email/images/blog/stripe-to-email/cover.png) It's great to receive [Stripe](https://stripe.com/) emails on new payments however it's harder to differentiate new customers from recurring when you are mostly selling subscriptions. In this short tutorial we will set up a function that receives a webhook from Stripe and transforms it into an email which is then dispatched via [Mailgun](https://www.mailgun.com/). ## [Prerequisites](#prerequisites) - [Webhook Relay account](https://my.webhookrelay.com) - [Stripe](https://stripe.com) - [Mailgun](https://www.mailgun.com) ## [Create Webhook Relay bucket](#create-webhook-relay-bucket) In order to start receiving webhooks and doing things with them you will first need to create a bucket here [https://my.webhookrelay.com/buckets](https://my.webhookrelay.com/buckets). Once created, copy the input URL which looks like `https://xyz.hooks.webhookrelay.com`, we will need it for Stripe. ## [Configure Stripe](#configure-stripe) When you have logged into your Stripe dashboard, head to webhook admin page [https://dashboard.stripe.com/webhooks](https://dashboard.stripe.com/webhooks) and add a new endpoint with the input URL from the previous step. ![stripe webhooks](https://webhookrelay.com/blog/stripe-webhook-to-email/images/blog/stripe-to-email/create-webhook.png) It should listen for `customer.subscription.created` events. ## [Create a function](#create-a-function) Our function will do several things, in this example I will filter out all subscriptions that or on a plan named "free", prepare some variables from the webhook and send an email via Mailgun. 1. First, go to function's "config variables" tab and set two variables: - **api_key** - your Mailgun API key ([https://app.mailgun.com/app/account/security/api_keys](https://app.mailgun.com/app/account/security/api_keys)) - **domain** - your Mailgun domain (for example mg.example.com) 2. Create the function body and set your sender and recipient emails (replace _[from-address@example.com](https://webhookrelay.com/blog/stripe-webhook-to-email//mailto:from-address@example.com)_ and _[to-address@example.com](https://webhookrelay.com/blog/stripe-webhook-to-email//mailto:to-address@example.com)_): ``` mailgun = require('mailgun') json = require('json') -- First, decoding the stripe payload local request_payload, err = json.decode(r.RequestBody) if err then error(err) end -- You can use product IDs or any other fields that you find useful here local plan = request_payload["data"]["object"]["plan"]["nickname"] local amount = request_payload["data"]["object"]["plan"]["amount"] local subscription_id = request_payload["data"]["object"]["id"] local customer_id = request_payload["data"]["object"]["customer"] -- If you have a free plan, you can ignore it if plan == "Free" then -- request is not important, don't forward it r:StopForwarding() return end -- preparing the mailgun client client err = mailgun.initialize(cfg:GetValue('domain'), cfg:GetValue('api_key'), 'us') if err then error(err) end -- Preparing the email message! local text = string.format([[ Horray! Customer subscribed to '%s' ($%s). Subscription: https://dashboard.stripe.com/subscriptions/%s Customer: https://dashboard.stripe.com/customers/%s Kind regards, Webhook Relay Function ]], plan, amount/100, subscription_id, customer_id) err = mailgun.send('from-address@example.com', 'Hooray! New Paying Customer!', text, 'to-address@example.com') if err then error(err) end r:SetRequestBody('sent') ``` ## [Attaching function to the Bucket's Input](#attaching-function-to-the-buckets-input) Now, let's go back to our Bucket, click on the Input and attach this function: ![Attach function to input](https://webhookrelay.com/blog/stripe-webhook-to-email/images/blog/stripe-to-email/attach-func-to-input.png) Once function is in place, you are ready to start receiving the emails. ## [Wrap up](#wrap-up) That's it, on the next new subscription you will receive an email with the details! :) Some ideas for the follow up work: - Function to detect plan downgrades and send you an email - Detect churned customers - Email you when payment charge fails Enjoy! --- --- title: Introducing Service Connections | WebhookRelay meta: "og: title": "Introducing Service Connections" description: Connect AWS, GCP, and Azure services through Webhook Relay buckets with fast, flexible cloud-to-cloud routing. url: https://webhookrelay.com/blog/introducing_service_connections.md file: /blog/introducing_service_connections.md --- ![Stripes](https://webhookrelay.com/blog/introducing_service_connections/images/stripes.svg) # **Introducing Service Connections** Connect AWS, GCP, and Azure services through Webhook Relay buckets with fast, flexible cloud-to-cloud routing. Service Connections are now live in Webhook Relay. This new feature makes it easy to connect cloud services to your buckets and move events between systems without building custom glue code. You can ingest events from cloud services, transform them, and forward them anywhere you need. ![Service Connections](https://webhookrelay.com/blog/introducing_service_connections/images/docs/sc/add_sc.png) We now support integrations across **GCP**, **AWS**, and **Azure**, with ready-to-use connectors for popular event and storage services. You can route data across providers, bridge cloud-to-cloud workflows, and send events to internal or public destinations. ![Service Connections dashboard](https://webhookrelay.com/blog/introducing_service_connections/images/docs/sc/service_connections.png) Service Connections are built to be the fastest path to production: - Connect cloud credentials once - Add inputs and outputs to any bucket - Bridge third-party services in minutes - Add Functions when you need payload transformation ![Create an input](https://webhookrelay.com/blog/introducing_service_connections/images/docs/sc/add_input.png) This is just the start. We are actively adding more services and connectors to make Webhook Relay the easiest and fastest way to connect third-party services together. You can explore setup steps and examples in the [Service Connections docs](https://webhookrelay.com/blog/introducing_service_connections/docs/service-connections/). Example use cases: - Bridge GCP Pub/Sub to AWS SQS - Send all Stripe webhooks to GCS or S3 storage for archival. Bigquery can then query the data for analytics - Subscribe to internal SQS queues such as AWS Guardduty to send Slack/Discord notifications --- --- title: Trigger a Self-Hosted AI Agent with Webhooks (No Public IP) meta: "og: title": "Trigger a Self-Hosted AI Agent with Webhooks (No Public IP)" description: Run a local AI agent on a Raspberry Pi or Mac mini behind NAT and trigger it with GitHub or Stripe webhooks — no port forwarding and no public IP. url: https://webhookrelay.com/blog/self-hosted-ai-agent-webhooks.md file: /blog/self-hosted-ai-agent-webhooks.md --- ![Stripes](https://webhookrelay.com/blog/self-hosted-ai-agent-webhooks/images/stripes.svg) # **Trigger a Self-Hosted AI Agent with Webhooks (No Public IP)** Run a local AI agent on a Raspberry Pi or Mac mini behind NAT and trigger it with GitHub or Stripe webhooks — no port forwarding and no public IP. ![Self-hosted AI agent on a Raspberry Pi receiving webhooks](https://webhookrelay.com/blog/self-hosted-ai-agent-webhooks/images/blog/eve-agent/cover.jpg) Self-hosted AI agents are having a moment. A Mac mini or Raspberry Pi is plenty to run an always-on agent — the model is an API call away, and everything that matters (your tools, your data, your state) stays on hardware you own. But a **local AI agent** that can only be poked over the LAN misses the point of being always-on. The interesting work arrives as events from the outside world: a GitHub issue is opened, a Stripe payment fails, Grafana fires an alert. Those are **webhooks** — and GitHub can't POST to a box behind NAT with no public IP. Opening a port and pointing dynamic DNS at your home network is exactly the kind of thing you shouldn't do to the machine that runs an agent with shell access. We built a small, fully working example that solves this properly: [**webhookrelay-eve-agent-example**](https://github.com/webhookrelay/webhookrelay-eve-agent-example) — an agent built with [eve](https://vercel.com/eve), Vercel's filesystem-first framework for durable agents, triggered by any webhook from anywhere, with zero inbound ports. ## [The architecture](#the-architecture) ![Self-hosted AI agent architecture: webhooks cross the NAT/firewall network boundary from Webhook Relay Cloud to a webhookrelayd sidecar and eve agent running on a home network or office LAN](https://webhookrelay.com/blog/self-hosted-ai-agent-webhooks/images/blog/eve-agent/architecture.svg) [Webhook Relay](https://webhookrelay.com/blog/self-hosted-ai-agent-webhooks/) provides the stable public HTTPS endpoint. The `webhookrelayd` sidecar runs next to the agent in docker-compose and keeps an _outbound_ connection to it, so every webhook is pulled down to the agent over the internal compose network. The box stays invisible to the internet — this is the same [webhook forwarding](https://webhookrelay.com/blog/self-hosted-ai-agent-webhooks/features/webhook-to-internal-server/) that powers local webhook development, used as permanent infrastructure. ## [The agent: three small files](#the-agent-three-small-files) ![eve agent project structure — instructions.md, agent.ts and a tools directory — powered by Webhook Relay](https://webhookrelay.com/blog/self-hosted-ai-agent-webhooks/images/blog/eve-agent/eve-webhookrelay.png) eve agents are just files. The model config points at any AI SDK provider — the example calls Claude through [Lightning AI](https://lightning.ai)'s Anthropic-compatible API: ``` // agent/agent.ts const lightning = createAnthropic({ baseURL: process.env.LIGHTNING_BASE_URL ?? "https://lightning.ai/v1", apiKey: process.env.LIGHTNING_API_KEY ?? "", }); export default defineAgent({ model: lightning(process.env.LIGHTNING_MODEL ?? "claude-sonnet-4-6"), }); ``` A custom **channel** gives the agent its webhook endpoint. Each delivery starts a fresh, durable agent session with the payload (and the provider headers Webhook Relay preserves, like `x-github-event`) as the message: ``` // agent/channels/webhook.ts export default defineChannel({ routes: [ POST("/eve/v1/webhook", async (req, { send }) => { const body = await req.text(); const session = await send( \`A webhook just arrived. Triage it and save a report.\n\n${body}\`, { auth: null, continuationToken: \`webhook-${crypto.randomUUID()}\` }, ); return Response.json({ ok: true, sessionId: session.id }); }), ], }); ``` And a typed **tool** is what the model does about it — here, writing a triage report to disk. This is the part you'd swap for a Slack message, a database insert or a home-automation call: ``` // agent/tools/save_report.ts export default defineTool({ description: "Save a triage report for a webhook event as a markdown file.", inputSchema: z.object({ slug: z.string(), markdown: z.string() }), async execute({ slug, markdown }) { /* write reports/-.md */ }, }); ``` ## [What it does](#what-it-does) Send a test event to your bucket's public URL: ``` curl -X POST https://xxxxx.hooks.webhookrelay.com/ \ -H 'content-type: application/json' \ -H 'x-github-event: issues' \ -d '{"action":"opened","issue":{"number":42,"title":"Payments webhook drops events during deploys", ...}}' ``` A few seconds later there's a report in `reports/`. This is real output from the example agent: > **GitHub Issue Opened — acme/shop #42** · Severity: info A new GitHub issue was opened on the `acme/shop` repository by `octocat`. Although classified as `info`, the subject matter is noteworthy — dropped payment webhook events during deploys could indicate a reliability risk worth investigating proactively._Suggested next action:_ review and assign issue #42 to the payments team; investigate whether there is a known deploy-window gap in webhook event handling. Point GitHub, Stripe, Shopify or your monitoring at the same URL and every event flows through — the agent reads the payload, decides a severity and writes up what happened and what to do next. ## [Running it on a Raspberry Pi or Mac mini](#running-it-on-a-raspberry-pi-or-mac-mini) The whole stack is one `docker compose up --build -d` — the agent container plus the `webhookrelayd` sidecar. The image is built on multi-arch `node:24-slim`, so the same compose file works on a Raspberry Pi (arm64), a Mac mini acting as a quiet home AI server, a NAS or a VM behind a corporate firewall. Two things worth turning on for hardware that lives at home: - **[Durable retries](https://webhookrelay.com/blog/self-hosted-ai-agent-webhooks/features/durable-retries/)** on the bucket — if the machine sleeps, reboots or loses connectivity, events queue in the cloud and deliver when the sidecar reconnects, for up to 30 days. - A `WEBHOOK_SECRET` (supported by the example) so the agent's channel rejects anything that didn't come through your relay. The [README](https://github.com/webhookrelay/webhookrelay-eve-agent-example) has the full quick start: create a bucket with an internal output pointing at `http://agent:3000/eve/v1/webhook`, drop your token into `.env.local`, and compose up. For local development you can skip Docker entirely and forward the bucket to `eve dev` with the [relay CLI](https://webhookrelay.com/blog/self-hosted-ai-agent-webhooks/docs/installation/cli/). The example triages events because it's a useful default that needs no extra API keys — but the wiring is the point. Swap `agent/instructions.md` and the tools, and the same skeleton becomes a code-review agent, an on-call assistant or the brain of your home automation: any webhook in the world can now reach the agent in your drawer. --- --- title: Test Stripe Connect Webhooks Locally | WebhookRelay meta: "og: title": "Test Stripe Connect Webhooks Locally" description: Test Stripe Connect webhooks locally on localhost without deploying. Capture connected-account events, forward to your handler, and verify the Stripe-Signature. url: https://webhookrelay.com/blog/receive-stripe-connect-webhooks-locally.md file: /blog/receive-stripe-connect-webhooks-locally.md --- ![Stripes](https://webhookrelay.com/blog/receive-stripe-connect-webhooks-locally/images/stripes.svg) # **Test Stripe Connect Webhooks Locally** Test Stripe Connect webhooks locally on localhost without deploying. Capture connected-account events, forward to your handler, and verify the Stripe-Signature. ![Test Stripe Connect Webhooks Locally (Connected-Account Webhooks on localhost)](https://webhookrelay.com/blog/receive-stripe-connect-webhooks-locally/images/blog/heroes/localhost.jpg) You are building a Stripe Connect platform — a marketplace or SaaS that onboards merchants as connected accounts — and you need to see your handler react to a real `payment_intent.succeeded` or `account.updated` event that happened **on one of your connected accounts**. The problem is immediate: Stripe will only POST to a **public URL**, and your handler is running on `localhost:8080`. Stripe has no way to reach it. The usual workarounds are painful. Deploying to staging for every code change is slow. Copying a sample payload from the docs into curl gives you a _guess_ at the real request — and worse, it omits the one field that makes Connect different: the `account` field telling you which connected account the event belongs to. What you want is to **test Stripe Connect webhooks locally** — real connected-account events, hitting your local handler, with a URL that does not change every time you restart. This guide focuses on the Connect-specific bits. For the base Stripe flow, see [Receiving Stripe webhooks on localhost](https://webhookrelay.com/blog/receive-stripe-connect-webhooks-locally/blog/receiving-stripe-webhooks-localhost/). ## [What makes Connect webhooks different](#what-makes-connect-webhooks-different) A regular Stripe webhook endpoint listens to events on **your own account**. A **Connect** endpoint listens to events on the **connected accounts** your platform manages. Three things change: - **Scope:** the endpoint is configured to listen to "events on Connected accounts," so it receives events triggered by resources that live inside your connected accounts. - **The `account` field:** every connected-account event includes a top-level `account` field (for example `acct_1A2b3C...`) identifying which connected account the event came from. Your handler must read this to route the event to the right merchant. - **A separate signing secret:** the Connect endpoint has its **own** signing secret, distinct from any account-level endpoint you already have. Using the wrong secret is the single most common reason signature verification fails. You only need **one** Connect endpoint for all of your connected accounts — Stripe does not require a separate endpoint per merchant. ## [Why testing Connect webhooks locally is tricky](#why-testing-connect-webhooks-locally-is-tricky) Stripe sits on the public internet; your dev machine usually does not. It is behind a router, a corporate firewall, or both, with no public IP and no inbound ports open. So you need something in the middle: a public endpoint Stripe can hit, that relays each request down to your laptop without you opening a single firewall port. That is what [Webhook Relay](https://my.webhookrelay.com/register) does — and unlike a random tunnel URL, the endpoint is **stable**, so you configure Stripe once and never touch it again. ## [Step 1: Inspect the real payload with Webhook Bin](#step-1-inspect-the-real-payload-with-webhook-bin) Before you write any handler code, find out what a connected-account event actually looks like. Open the free [Webhook Bin](https://webhookrelay.com/blog/receive-stripe-connect-webhooks-locally/webhook-bin/) — no signup — and you get an instant public URL. 1. Copy the Webhook Bin URL. 2. In the Stripe Dashboard, go to **Developers → Webhooks → Add endpoint**. 3. Paste the URL into **Endpoint URL**, add a description, then choose **Listen to events on Connected accounts** (this is what makes it a Connect endpoint). 4. Select events — for example `payment_intent.succeeded`, `account.updated`, `payout.paid` — and add the endpoint. Trigger an action on a connected account (in test mode, complete a payment on a connected account, or update an account) and inspect the captured request in Webhook Bin: the full JSON body, the query string, and every header. Confirm you can see the top-level **`account`** field and the **`Stripe-Signature`** header. That `account` field is your proof the endpoint is scoped to Connect, not your platform account. For more on this approach, see [How to test webhooks](https://webhookrelay.com/blog/receive-stripe-connect-webhooks-locally/blog/how-to-test-webhooks/) and [What is a webhook](https://webhookrelay.com/blog/receive-stripe-connect-webhooks-locally/blog/what-is-webhook/). ## [Step 2: Forward the events to localhost with the relay agent](#step-2-forward-the-events-to-localhost-with-the-relay-agent) Once you know the payload, route those same events into your local handler. [Sign up for Webhook Relay](https://my.webhookrelay.com/register), install the `relay` agent (CLI or Docker), and create a bucket — say `stripe-connect`. The bucket gives you a stable public input endpoint. Start forwarding to your local server: ``` relay forward --bucket stripe-connect http://localhost:8080/webhook ``` The agent opens an **outbound** connection to Webhook Relay and streams every incoming request down to `http://localhost:8080/webhook`. Because the connection is outbound, there are **no firewall ports to open and no public IP needed** — this works from your laptop, behind a corporate proxy, or inside a Kubernetes cluster. Running in Docker? The same command works in the official `webhookrelay/webhookrelayd` image. Full details are in the [localhost forwarding docs](https://webhookrelay.com/blog/receive-stripe-connect-webhooks-locally/docs/webhooks/internal/localhost/). Now set the Connect endpoint's URL in **Developers → Webhooks** to your Webhook Relay endpoint (or create it there from the start, still with **Listen to events on Connected accounts** selected). Trigger a connected-account event and watch it arrive on `localhost`. ## [Stripe Connect-specific configuration and quirks](#stripe-connect-specific-configuration-and-quirks) A few Connect details worth knowing: - **Pick the Connect scope, not "Your account":** when adding the endpoint you must explicitly choose **Listen to events on Connected accounts**. An account-scoped endpoint will never see your connected accounts' events. - **One endpoint covers all merchants:** the same Connect endpoint receives events for every connected account, distinguished by the `account` field. Route on that field in your handler. - **Grab the Connect endpoint's signing secret:** open the endpoint in the Dashboard and reveal its **signing secret** (it starts with `whsec_`). This is _different_ from your account endpoint's secret — store it separately. - **Test mode vs live mode:** webhook endpoints and their signing secrets are per-mode. Set up the endpoint in test mode while developing, then add the live endpoint and its own secret before going live. - **Replay from Stripe:** the Dashboard lets you **resend** any past delivery to your endpoint with the original payload and signature — ideal for re-running a real connected-account event against your local handler. ## [Step 3: Verify the Stripe Connect webhook signature](#step-3-verify-the-stripe-connect-webhook-signature) Stripe signs every request so you can confirm it really came from Stripe. It sends the digest in the **`Stripe-Signature`** header — an **HMAC-SHA256** computed over the string `timestamp.payload` using the endpoint's signing secret. For a Connect endpoint, that means the **Connect endpoint's** signing secret, not your account endpoint's. Your handler should recompute the HMAC over the **raw** body (the exact bytes received, before any JSON parser reshapes them), check the timestamp is recent, and compare in constant time before trusting the payload. The Stripe SDKs do this for you via `Webhook.constructEvent` / `Webhook.constructEvent` equivalents — just be sure you pass the **Connect** secret. Mixing up the account and Connect secrets is the classic cause of "signature verification failed." To sanity-check the underlying HMAC, paste a captured body, your secret, and the received signature into the free [Stripe signature verifier](https://webhookrelay.com/blog/receive-stripe-connect-webhooks-locally/verify-stripe-webhook-signature/). For language-specific code and the common pitfalls (reading the body after a JSON parser has consumed it, timing-safe comparison, the timestamp tolerance), read [Verify a webhook signature](https://webhookrelay.com/blog/receive-stripe-connect-webhooks-locally/blog/verify-webhook-signature/). ## [Replay and iterate](#replay-and-iterate) This is where local development gets fast: - **Replay from Stripe** via the endpoint's **resend** action to re-run a real connected-account delivery against your handler. - **Replay from Webhook Relay** — past requests are stored on your bucket, so you can resend a captured event without touching Stripe at all. - **Iterate on your handler** by editing code and replaying the same delivery until your routing on the `account` field behaves correctly. No deploys just to test a single code path. Because the Webhook Relay endpoint is stable, you can stop and restart the agent, reboot your machine, or come back next week — the Stripe Connect configuration never needs to change. ## [Get started](#get-started) 1. Inspect a real connected-account payload in the free [Webhook Bin](https://webhookrelay.com/blog/receive-stripe-connect-webhooks-locally/webhook-bin/) — no signup needed. 2. [Create a Webhook Relay account](https://my.webhookrelay.com/register), install the agent, and run `relay forward --bucket stripe-connect http://localhost:8080/webhook`. 3. Add a Stripe endpoint with **Listen to events on Connected accounts**, point it at the stable URL, trigger an event, and watch it hit `localhost`. You will be testing real Stripe Connect events against your local handler in a few minutes — no deploys, no open firewall ports, and a URL you configure exactly once. --- --- title: Stripe CLI Alternative for Webhook Testing | WebhookRelay meta: "og: title": "Stripe CLI Alternative for Webhook Testing" description: A Stripe CLI alternative for teams: one stable URL that receives Stripe and every other provider's webhooks, shareable across the whole team. url: https://webhookrelay.com/blog/stripe-cli-alternative.md file: /blog/stripe-cli-alternative.md --- ![Stripes](https://webhookrelay.com/blog/stripe-cli-alternative/images/stripes.svg) # **Stripe CLI Alternative for Testing Webhooks Across Every Provider** A Stripe CLI alternative for teams: one stable URL that receives Stripe and every other provider's webhooks, shareable across the whole team. ![Stripe CLI Alternative for Testing Webhooks Across Every Provider](https://webhookrelay.com/blog/stripe-cli-alternative/images/blog/heroes/alternative.jpg) The [Stripe CLI](https://docs.stripe.com/stripe-cli) is the official Stripe tool, and for local Stripe development it's excellent. Run `stripe listen --forward-to localhost:4242/webhook` and it opens a direct connection to Stripe, forwards every event tied to your account down to your local server, and prints a webhook signing secret (`whsec_...`) you drop into your config. No public URL, no tunnel to configure — it just works. So why look for a Stripe CLI alternative? Because the moment your needs grow past _one developer testing Stripe on their own laptop_, the CLI's model starts to pinch. This post explains where the Stripe CLI shines, where it doesn't, and how [Webhook Relay](https://my.webhookrelay.com/register) covers the gaps. ## [TL;DR](#tldr) - **Working alone, on Stripe only, on your own machine?** The Stripe CLI is the right tool. It's official, fast, and prints a signing secret instantly. - **Need the same URL for Stripe _and_ GitHub, Shopify, Twilio, your CI?** The CLI is Stripe-only; every other provider needs a different tool. Webhook Relay gives you one URL for all of them. - **Want a stable URL a teammate or a staging environment can use?** The CLI session lives on your machine. A Webhook Relay bucket is a stable, shareable public endpoint. - **Need to forward into private production infrastructure, not just dev?** The CLI is a development tool. The relay agent forwards into localhost _and_ private servers, behind firewalls, with no inbound ports. ## [Stripe CLI vs Webhook Relay](#stripe-cli-vs-webhook-relay) | | Stripe CLI | Webhook Relay | | --- | --- | --- | | Providers supported | Stripe only | [Stripe + any provider](https://webhookrelay.com/blog/stripe-cli-alternative/blog/how-to-test-webhooks/) | | Prints a Stripe signing secret | Yes | Use your Stripe endpoint secret | | Stable, shareable URL | No (local session) | [Yes (per-bucket endpoint)](https://webhookrelay.com/blog/stripe-cli-alternative/webhooks/) | | Forward to localhost | Yes | Yes ([relay agent](https://webhookrelay.com/blog/stripe-cli-alternative/docs/webhooks/internal/localhost/)) | | Forward to **private production infra** | No (dev tool) | Yes (relay agent, no open ports) | | Fan-out to multiple destinations | No | [Yes](https://webhookrelay.com/blog/stripe-cli-alternative/features/webhook-multiple-destinations/) | | Transformations | No | [JS / Lua + AI](https://webhookrelay.com/blog/stripe-cli-alternative/features/transform-webhooks/) | | Retries / replay from a dashboard | Limited | Yes | | Free request inspector | No | [Webhook Bin](https://webhookrelay.com/blog/stripe-cli-alternative/webhook-bin/) | _Details reflect publicly documented Stripe CLI behavior as of 2026; verify current capabilities on stripe.com._ ## [Where the Stripe CLI is strong](#where-the-stripe-cli-is-strong) Let's be fair — the Stripe CLI is genuinely good at its job: - **It's official and Stripe-native.** It understands Stripe events, can `stripe trigger` test events, and integrates with the rest of the CLI (logs, fixtures, resource commands). - **Zero URL plumbing.** `stripe listen` opens a direct connection to Stripe; there's no public endpoint to register, no DNS, nothing to expose. - **Instant signing secret.** The `whsec_` secret it prints stays stable across restarts of the listen command, so signature verification works locally without touching the Dashboard. If you're a single developer iterating on Stripe payment flows on your own machine, the Stripe CLI is the better pick, and we'd point you straight to it. ## [Where the Stripe CLI runs out of room](#where-the-stripe-cli-runs-out-of-room) ### [1. It's Stripe-only](#_1-its-stripe-only) This is the big one. The Stripe CLI forwards _Stripe_ events and nothing else. The instant your app also receives webhooks from GitHub, Shopify, Twilio, Slack, a payment processor, or your CI system, the CLI can't help — you reach for a separate tool for each provider, each with its own setup and its own throwaway URL. Real applications rarely live on a single provider. A typical SaaS consumes Stripe for billing, GitHub for deploys, and a couple of SaaS webhooks for good measure. Juggling one mechanism per provider is exactly the kind of friction Webhook Relay removes: **one stable URL receives all of them.** ### [2. The URL lives on your machine](#_2-the-url-lives-on-your-machine) `stripe listen` is bound to the developer running it. There's no stable URL a teammate can configure once, no endpoint a shared **staging environment** can point at, and nothing a QA engineer or a contractor can reuse. When the laptop sleeps, the session ends. That's by design — it's a local dev tool. But teams frequently want a single, durable endpoint that several people (and several environments) share. A Webhook Relay [bucket](https://webhookrelay.com/blog/stripe-cli-alternative/webhooks/) is exactly that: a stable public URL you configure in Stripe once, and that anyone on the team — or a staging box — can forward from. ### [3. It's for development, not production routing](#_3-its-for-development-not-production-routing) The Stripe CLI is a development aid. It is not a path to delivering Stripe events into **private production infrastructure** — an internal service with no public IP, a handler inside a Kubernetes cluster, a server behind a corporate firewall. There's no production story there at all. This is the core of what Webhook Relay does. ## [Where Webhook Relay is different](#where-webhook-relay-is-different) ### [One URL, every provider, forwarded anywhere](#one-url-every-provider-forwarded-anywhere) A Webhook Relay bucket gives you a stable public endpoint. Point Stripe at it (and GitHub, and Shopify, and anything else), then run the relay agent to forward those events wherever you need: ``` # forward to your local Stripe handler relay forward --bucket stripe http://localhost:4242/webhook # or into a private production service with no public IP relay forward --bucket stripe http://payments.internal:9000/stripe ``` The agent makes an **outbound** connection, so there are no firewall ports to open and no public IP required — it works from your laptop, behind a corporate proxy, or inside a [Kubernetes cluster](https://webhookrelay.com/blog/stripe-cli-alternative/features/webhook-kubernetes-integration/). Same URL for development and for production routing into private infra. Full setup is in the [localhost forwarding docs](https://webhookrelay.com/blog/stripe-cli-alternative/docs/webhooks/internal/localhost/), and there's a focused walkthrough in [Receiving Stripe webhooks on localhost](https://webhookrelay.com/blog/stripe-cli-alternative/blog/receiving-stripe-webhooks-localhost/). ### [Inspect, transform, fan-out and retry](#inspect-transform-fan-out-and-retry) Because the events flow through Webhook Relay, you get capabilities the CLI doesn't have: - **Inspect the raw payload** in the free [Webhook Bin](https://webhookrelay.com/blog/stripe-cli-alternative/webhook-bin/) — full body and every header, including `Stripe-Signature`, no account required. - **[Fan a single webhook out to multiple destinations](https://webhookrelay.com/blog/stripe-cli-alternative/features/webhook-multiple-destinations/)** — your local handler _and_ a staging service, for example. - **[Reshape payloads in flight](https://webhookrelay.com/blog/stripe-cli-alternative/features/transform-webhooks/)** with JavaScript or Lua. - **Replay and retry** captured events from the dashboard, so you can re-run a real Stripe event against your handler without re-triggering it in Stripe. ### [Signature verification still works](#signature-verification-still-works) You keep verifying Stripe's signature exactly as you would normally — compute the HMAC over the raw body using your endpoint's signing secret and compare the `Stripe-Signature` header. (Note that the secret comes from your Stripe **Dashboard webhook endpoint**, not the CLI's local `whsec_`.) To sanity-check an implementation, paste a captured body and secret into the free [HMAC verifier](https://webhookrelay.com/blog/stripe-cli-alternative/hmac-verification/), and for language-specific code read [Verify a webhook signature](https://webhookrelay.com/blog/stripe-cli-alternative/blog/verify-webhook-signature/). ## [When to pick which](#when-to-pick-which) - **Pick the Stripe CLI** if you're a single developer doing pure local Stripe work and never need a shared URL or private-infra delivery. It's the official tool and it's great at that. - **Pick Webhook Relay** if you need one stable URL for Stripe _and_ every other provider, want to share it across a team or a staging environment, or need to forward webhooks into localhost _and_ private production infrastructure — with fan-out, transformations and replay built in. [Start free](https://my.webhookrelay.com/register) or [compare plans](https://webhookrelay.com/blog/stripe-cli-alternative/pricing/). Want to see a Stripe payload first? [Inspect one in the browser](https://webhookrelay.com/blog/stripe-cli-alternative/webhook-bin/) — no signup needed. ![Stripes](https://webhookrelay.com/blog/stripe-cli-alternative/images/stripes-dark.svg) ## **Start forwarding webhooks in minutes ** Connect a source, pick a destination, and Webhook Relay handles delivery, retries and transforms. Set up your first webhook in under five minutes. [Start for free ->](https://my.webhookrelay.com/register) Free plan · No credit card required · 7-day money-back guarantee on paid plans --- --- title: Archive AWS GuardDuty Findings to GCP Cloud Storage meta: "og: title": "Archive AWS GuardDuty Findings to GCP Cloud Storage" description: Route AWS GuardDuty security findings to Google Cloud Storage using Webhook Relay Service Connections for long-term retention and cross-cloud analysis url: https://webhookrelay.com/blog/guardduty-to-gcs-archival.md file: /blog/guardduty-to-gcs-archival.md --- ![Stripes](https://webhookrelay.com/blog/guardduty-to-gcs-archival/images/stripes.svg) # **Archive AWS GuardDuty Findings to GCP Cloud Storage** Route AWS GuardDuty security findings to Google Cloud Storage using Webhook Relay Service Connections for long-term retention and cross-cloud analysis AWS GuardDuty monitors your AWS accounts for malicious activity and unauthorized behavior. But what if your analytics stack lives in GCP? Maybe you run BigQuery for security analysis or Chronicle for SIEM. Getting GuardDuty findings into GCP usually means building custom Lambda functions, managing cross-cloud credentials, and maintaining glue code. Webhook Relay [Service Connections](https://webhookrelay.com/blog/guardduty-to-gcs-archival/docs/service-connections/) let you route GuardDuty findings to GCP Cloud Storage in minutes, no code required. ## [Architecture Overview](#architecture-overview) The data flow: ``` AWS GuardDuty → EventBridge → SQS Queue → Webhook Relay → GCP Cloud Storage ``` GuardDuty publishes findings to Amazon EventBridge. An EventBridge rule routes them to an SQS queue. Webhook Relay polls the queue and stores each finding as a JSON file in your GCS bucket. You can also publish to AWS S3 if you prefer, just in this case we want to access them through BigQuery afterwards. ## [Why This Approach?](#why-this-approach) - **No custom code.** No Lambda functions to maintain, no credential rotation scripts, no deployment pipelines. - **Long-term retention.** GCS lifecycle policies move findings to Coldline or Archive storage automatically. Keep years of security data cheaply. - **Analytics-ready.** BigQuery can query your findings directly from GCS. Build dashboards, run threat hunting queries, or feed data into Chronicle. - **Reliable delivery.** SQS provides durable message queuing. Webhook Relay handles retries. ## [AWS Setup](#aws-setup) ### [Step 1: Create an SQS Queue](#step-1-create-an-sqs-queue) First, create an [SQS queue](https://webhookrelay.com/blog/guardduty-to-gcs-archival/docs/service-connections/aws_sqs/) to receive GuardDuty findings: 1. Go to **AWS Console → SQS → Create queue** 2. Choose **Standard** queue type 3. Name it `guardduty-findings` 4. Keep default settings and create the queue Copy the **Queue URL**. It looks like: ``` https://sqs.us-east-1.amazonaws.com/123456789012/guardduty-findings ``` ### [Step 2: Create an EventBridge Rule](#step-2-create-an-eventbridge-rule) EventBridge routes GuardDuty findings to your SQS queue: 1. Go to **AWS Console → EventBridge → Rules → Create rule** 2. Name: `guardduty-to-sqs` 3. Event bus: `default` 4. Rule type: **Rule with an event pattern** For the event pattern, select: - AWS service: **GuardDuty** - Event type: **GuardDuty Finding** Or use this custom pattern to capture all GuardDuty findings: ``` { "source": ["aws.guardduty"], "detail-type": ["GuardDuty Finding"] } ``` For the target: - Target type: **AWS service** - Select target: **SQS queue** - Queue: `guardduty-findings` Create the rule. EventBridge will now route all GuardDuty findings to your SQS queue. ### [Step 3: Create an IAM User for Webhook Relay](#step-3-create-an-iam-user-for-webhook-relay) Webhook Relay needs credentials to poll your SQS queue: 1. Go to **AWS Console → IAM → Users → Create user** 2. Name: `webhookrelay-sqs-reader` 3. Attach a policy with these permissions: ``` { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "sqs:ReceiveMessage", "sqs:DeleteMessage", "sqs:GetQueueAttributes" ], "Resource": "arn:aws:sqs:us-east-1:123456789012:guardduty-findings" } ] } ``` 1. Create an access key and save the **Access Key ID** and **Secret Access Key** ## [GCP Setup](#gcp-setup) We will be using [GCP GCS service connection](https://webhookrelay.com/blog/guardduty-to-gcs-archival/docs/service-connections/gcp_gcs/) for this. ### [Step 1: Create a GCS Bucket](#step-1-create-a-gcs-bucket) 1. Go to **GCP Console → Cloud Storage → Create bucket** 2. Name: `guardduty-archive` (or your preferred name) 3. Choose your region and storage class 4. Create the bucket ### [Step 2: Create a Service Account](#step-2-create-a-service-account) 1. Go to **GCP Console → IAM → Service Accounts → Create** 2. Name: `webhookrelay-gcs-writer` 3. Grant the role: **Storage Object Creator** (`roles/storage.objectCreator`) 4. Create a JSON key and download it ## [Webhook Relay Setup](#webhook-relay-setup) Connect both clouds through Webhook Relay. ### [Step 1: Add AWS Service Connection](#step-1-add-aws-service-connection) 1. Go to [Webhook Relay Service Connections](https://my.webhookrelay.com/service-connections) 2. Click **Add Connection** 3. Select **AWS** 4. Enter your IAM user credentials: - Access Key ID - Secret Access Key 5. Name it `aws-guardduty` and save ### [Step 2: Add GCP Service Connection](#step-2-add-gcp-service-connection) 1. Click **Add Connection** 2. Select **GCP** 3. Paste the contents of your service account JSON key 4. Name it `gcp-storage` and save ### [Step 3: Create a Bucket with SQS Input and GCS Output](#step-3-create-a-bucket-with-sqs-input-and-gcs-output) 1. Go to [Buckets](https://my.webhookrelay.com/buckets) and create a new bucket 2. Name it `guardduty-archive` Add the **SQS Input**: 1. Click **Add Input → AWS SQS** 2. Select your `aws-guardduty` connection 3. Enter the queue URL: `https://sqs.us-east-1.amazonaws.com/123456789012/guardduty-findings` Add the **GCS Output**: 1. Click **Add Output → GCP Cloud Storage** 2. Select your `gcp-storage` connection 3. Bucket name: `guardduty-archive` 4. Prefix: `findings/` ![Selecting your output](https://webhookrelay.com/blog/guardduty-to-gcs-archival/images/blog/aws_guard_duty_gcs/gcs_bucket.png) And then configure the bucket: ![Output configuration](https://webhookrelay.com/blog/guardduty-to-gcs-archival/images/blog/aws_guard_duty_gcs/output.png) Webhook Relay will now poll your SQS queue and store each GuardDuty finding in GCS. ## [Verify It Works](#verify-it-works) ### [Generate a Test Finding](#generate-a-test-finding) GuardDuty can generate sample findings for testing: 1. Go to **AWS Console → GuardDuty → Settings** 2. Click **Generate sample findings** This creates test findings that flow through your pipeline. ### [Check GCS](#check-gcs) After a minute or two, check your GCS bucket. You should see files at: ``` findings/2026/03/06/.json ``` Each file contains the full GuardDuty finding with all metadata, severity scores, and resource details. ## [Optional: Transform Findings Before Storage](#optional-transform-findings-before-storage) To reshape the data or extract specific fields before storing, add a [Function](https://webhookrelay.com/blog/guardduty-to-gcs-archival/docs/webhooks/functions/) to your bucket: ``` const event = JSON.parse(r.body) const finding = event.detail // Create a simplified structure for analysis const archived = { id: finding.id, severity: finding.severity, type: finding.type, title: finding.title, description: finding.description, region: finding.region, accountId: finding.accountId, resourceType: finding.resource.resourceType, createdAt: finding.createdAt, updatedAt: finding.updatedAt, // Add custom fields archived_at: new Date().toISOString(), source: "aws-guardduty" } r.setBody(JSON.stringify(archived)) ``` This extracts key fields and adds metadata, making BigQuery queries simpler. ## [Query Findings with BigQuery](#query-findings-with-bigquery) Once your findings are in GCS, BigQuery can query them directly: ``` -- Create an external table pointing to your GCS bucket CREATE EXTERNAL TABLE \`project.dataset.guardduty_findings\` OPTIONS ( format = 'JSON', uris = ['gs://guardduty-archive/findings/*'] ); -- Query high-severity findings from the last 7 days SELECT id, type, severity, title, createdAt FROM \`project.dataset.guardduty_findings\` WHERE severity >= 7 AND DATE(createdAt) >= DATE_SUB(CURRENT_DATE(), INTERVAL 7 DAY) ORDER BY severity DESC; ``` ## [Use Cases](#use-cases) **Compliance and audit trails.** Many compliance frameworks require long-term retention of security events. GCS lifecycle policies can automatically transition old findings to Archive storage. **Cross-cloud SIEM.** If you're using Google Chronicle or another GCP-based SIEM, this pipeline feeds findings directly into your security analytics platform. **Cost optimization.** AWS GuardDuty retains findings for 90 days. For longer retention, exporting to GCS with lifecycle policies is often cheaper than other archival solutions. **Multi-cloud correlation.** Combine GuardDuty findings with security events from GCP and Azure in a single BigQuery dataset for unified threat analysis. ## [Conclusion](#conclusion) Routing GuardDuty findings to GCS used to require custom Lambda functions, cross-cloud IAM roles, and ongoing maintenance. With Webhook Relay Service Connections, setup takes minutes: 1. Create an SQS queue and EventBridge rule in AWS 2. Create a GCS bucket and service account in GCP 3. Connect them through Webhook Relay with an SQS input and GCS output Your findings are now archived in GCS, ready for BigQuery analysis, Chronicle ingestion, or compliance retention. [Sign up for Webhook Relay](https://my.webhookrelay.com/register) to start archiving GuardDuty findings. --- --- title: Introducing extra webhook packages | WebhookRelay meta: "og: title": "Introducing extra webhook packages" description: Purchase additional webhook capacity directly from your current plan tier url: https://webhookrelay.com/blog/extra-webhook-packages.md file: /blog/extra-webhook-packages.md --- ![Stripes](https://webhookrelay.com/blog/extra-webhook-packages/images/stripes.svg) # **Introducing extra webhook packages** Purchase additional webhook capacity directly from your current plan tier We're excited to announce a new feature that gives you more flexibility than ever before: you can now purchase extra webhook packages directly within your current plan tier. No need to upgrade to a higher plan just because you need a few more webhooks. ## [What's New](#whats-new) Until now, if you hit your monthly webhook limit, your only option was to upgrade to a higher-tier plan. While upgrading makes sense for some, many of our customers just needed a bit more capacity without all the additional features that come with larger plans. Now, you can add extra webhook packages to your Basic, Business, or Pro subscription using an interactive slider right on our [pricing page](https://webhookrelay.com/blog/extra-webhook-packages/pricing/). Simply adjust the slider to see how many extra webhooks you need and watch the price update in real-time. ## [Benefits](#benefits) **Flexibility** — Scale your webhook capacity up or down based on your actual needs. Running a big campaign this month? Add more webhooks. Quieter period? Scale back. **Cost-Effective** — Pay only for what you need. Adding extra webhooks is often more economical than jumping to the next plan tier if you don't need the additional features. **Self-Service** — No need to contact sales or wait for approval. Purchase extra capacity instantly, whenever you need it. **No Commitment** — Extra packages are added to your monthly subscription. Adjust them any time your needs change. ## [Pricing](#pricing) Extra webhook packages are available at the following rates: | Plan | Price | Webhooks per Unit | Maximum Extra | | --- | --- | --- | --- | | Basic | $5/month | 1,000 | +10,000 | | Business | $10/month | 10,000 | +200,000 | | Pro | $100/month | 1,000,000 | +50,000,000 | For example, if you're on the Basic plan with 5,000 webhooks included and need 3,000 more, simply add 3 units ($15/month) to get 8,000 total webhooks per month. ## [How It Works](#how-it-works) 1. Visit our [pricing page](https://webhookrelay.com/blog/extra-webhook-packages/pricing/) 2. Find your current plan (Basic, Business, or Pro) 3. Use the "Extra webhooks" slider to select how many additional webhooks you need 4. See the updated total price in real-time 5. Click "Sign up" to get started with your customized plan For existing customers, you can adjust your extra webhook packages from your account settings at any time. ## [Get Started](#get-started) Ready to scale your webhook capacity? Head over to our [pricing page](https://webhookrelay.com/blog/extra-webhook-packages/pricing/) and try the slider for yourself. See exactly how much extra capacity costs and find the perfect balance for your needs. If you have any questions about extra webhook packages or need help choosing the right configuration, don't hesitate to reach out to our support team. We're here to help! --- --- title: Azure Functions vs Webhook Relay | WebhookRelay meta: "og: title": "Azure Functions vs Webhook Relay" description: A practical comparison of Azure Functions and Webhook Relay for webhook processing, with code examples showing the difference in setup complexity. url: https://webhookrelay.com/blog/azure-functions-vs-webhook-relay.md file: /blog/azure-functions-vs-webhook-relay.md --- ![Stripes](https://webhookrelay.com/blog/azure-functions-vs-webhook-relay/images/stripes.svg) # **Azure Functions vs Webhook Relay: Why I stopped overengineering webhooks** A practical comparison of Azure Functions and Webhook Relay for webhook processing, with code examples showing the difference in setup complexity. Last month I spent 45 minutes setting up an Azure Function to forward GitHub webhooks to Discord. The actual logic was maybe 20 lines. The rest was fighting with resource groups, storage accounts, and deployment configs. There has to be a better way, I thought. Turns out there is. ## [The problem: GitHub push notifications to Discord](#the-problem-github-push-notifications-to-discord) Simple ask. When someone pushes code, post a message to our Discord channel. This should take five minutes, right? Let me walk you through both approaches so you can decide for yourself. ## [The Azure Functions route](#the-azure-functions-route) Here's what you need before writing a single line of code: - An Azure account with billing set up - Visual Studio Code with the Azure Functions extension - Node.js or .NET SDK - Azure Functions Core Tools installed locally Already tired? Me too. But let's keep going. ### [Create the project](#create-the-project) ``` npm install -g azure-functions-core-tools@4 func init GitHubToDiscord --javascript cd GitHubToDiscord func new --name WebhookHandler --template "HTTP trigger" ``` ### [Write the function](#write-the-function) Here's the JavaScript for `WebhookHandler/index.js`: ``` const https = require('https'); module.exports = async function (context, req) { context.log('GitHub webhook received'); const body = req.body; const pusher = body.pusher?.name || 'Unknown'; const repo = body.repository?.full_name || 'Unknown repo'; const branch = body.ref?.replace('refs/heads/', '') || 'unknown'; const commitCount = body.commits?.length || 0; const discordPayload = JSON.stringify({ content: \`**${pusher}** pushed ${commitCount} commit(s) to **${repo}** on branch \\`${branch}\\`\` }); const discordWebhookUrl = process.env.DISCORD_WEBHOOK_URL; await new Promise((resolve, reject) => { const url = new URL(discordWebhookUrl); const options = { hostname: url.hostname, path: url.pathname, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(discordPayload) } }; const request = https.request(options, (res) => { resolve(); }); request.on('error', reject); request.write(discordPayload); request.end(); }); context.res = { status: 200, body: "Webhook processed" }; }; ``` ### [Configure function.json](#configure-functionjson) ``` { "bindings": [ { "authLevel": "function", "type": "httpTrigger", "direction": "in", "name": "req", "methods": ["post"] }, { "type": "http", "direction": "out", "name": "res" } ] } ``` ### [Deploy it](#deploy-it) This is where things get fun: ``` az login az group create --name GitHubWebhookRG --location eastus az storage account create --name githubwebhookstorage --location eastus \ --resource-group GitHubWebhookRG --sku Standard_LRS az functionapp create --resource-group GitHubWebhookRG \ --consumption-plan-location eastus \ --runtime node --runtime-version 18 \ --functions-version 4 \ --name github-webhook-handler \ --storage-account githubwebhookstorage az functionapp config appsettings set --name github-webhook-handler \ --resource-group GitHubWebhookRG \ --settings "DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/..." func azure functionapp publish github-webhook-handler ``` That's a resource group, a storage account, and a function app just to transform one webhook. If everything works on the first try (it won't), you're looking at 15-30 minutes. --- ## [The Webhook Relay route](#the-webhook-relay-route) Same problem. Different approach. ### [Step 1: Create a bucket](#step-1-create-a-bucket) Go to [my.webhookrelay.com/buckets](https://my.webhookrelay.com/buckets) and click create. You get a public URL immediately. Something like `https://xxx.hooks.webhookrelay.com`. ### [Step 2: Write the function](#step-2-write-the-function) Go to [my.webhookrelay.com/functions](https://my.webhookrelay.com/functions), click "Create Function", paste this: ``` local json = require("json") local http = require("http") local payload, err = json.decode(r.RequestBody) if err then error(err) end local pusher = payload.pusher and payload.pusher.name or "Unknown" local repo = payload.repository and payload.repository.full_name or "Unknown repo" local branch = string.gsub(payload.ref or "", "refs/heads/", "") local commit_count = payload.commits and #payload.commits or 0 local discord_payload = { content = "**" .. pusher .. "** pushed " .. commit_count .. " commit(s) to **" .. repo .. "** on branch \`" .. branch .. "\`" } local encoded, err = json.encode(discord_payload) if err then error(err) end local resp, err = http.post("https://discord.com/api/webhooks/YOUR_WEBHOOK_ID/YOUR_TOKEN", { body = encoded, headers = { ["Content-Type"] = "application/json" } }) if err then error(err) end ``` ### [Step 3: Attach it](#step-3-attach-it) Go back to your bucket, click the input, select your function. ![Select function on input](https://webhookrelay.com/blog/azure-functions-vs-webhook-relay/images/docs/webhooks/functions/function-select.png) Done. Two, maybe three minutes. --- ## [The numbers](#the-numbers) | | Azure Functions | Webhook Relay | | --- | --- | --- | | Account setup | Azure subscription + billing | Email signup | | Infrastructure to create | Resource group, storage account, function app | None | | Files to manage | 3+ (index.js, function.json, host.json, package.json) | 1 code snippet | | Deployment | CLI commands or CI/CD pipeline | Paste in browser | | Time to working endpoint | 15-30 minutes | 2-3 minutes | | Cold starts | Yes | No | The Azure function is about 50 lines across multiple files. The Webhook Relay version is 25 lines in one place. --- ## [When Azure Functions makes sense](#when-azure-functions-makes-sense) I'm not saying Azure Functions is bad. It's overkill for webhooks, but it's the right choice when: - Your function needs to run for more than a few seconds - You're building something that talks to Cosmos DB, Service Bus, or other Azure services - You need a specific runtime version or language that Webhook Relay doesn't support - Your team already lives in the Azure ecosystem and has the tooling set up If you're building a real application backend, Azure Functions (or AWS Lambda, or Google Cloud Functions) is probably what you want. --- ## [When Webhook Relay makes sense](#when-webhook-relay-makes-sense) For webhooks specifically, Webhook Relay wins because: - No infrastructure to manage or pay for separately - No deployment pipeline to set up - No cold starts (webhook providers time out after a few seconds) - Built-in logging shows you exactly what came in and what went out - You can forward the same webhook to multiple places I use it for anything that's "receive webhook, maybe transform it, send it somewhere else." That covers most webhook use cases I run into. --- ## [The cold start problem](#the-cold-start-problem) This one bit me. Azure Functions on the consumption plan can take several seconds to wake up if they haven't run recently. Stripe, GitHub, and most other webhook providers expect a response within 5-10 seconds. If your function is cold, you might miss webhooks or trigger retries. You can pay for an always-on plan, but now you're spending real money on infrastructure for something that runs a few times a day. Webhook Relay doesn't have this problem. The function runs immediately because there's no container to spin up. --- ## [Try it](#try-it) If you're curious: 1. Sign up at [my.webhookrelay.com/register](https://my.webhookrelay.com/register) 2. Create a bucket 3. Write a function 4. Point your webhook at the endpoint You'll be done before you finish reading the Azure Functions quickstart guide. --- ## [Wrapping up](#wrapping-up) I still use Azure Functions for complex backend work. But for webhooks? I stopped overengineering it. The right tool depends on the problem. For "receive webhook, transform, forward" the simpler option is usually the better one. [Try Webhook Relay Functions](https://my.webhookrelay.com/register) ![Stripes](https://webhookrelay.com/blog/azure-functions-vs-webhook-relay/images/stripes-dark.svg) ## **Start forwarding webhooks in minutes ** Connect a source, pick a destination, and Webhook Relay handles delivery, retries and transforms. Set up your first webhook in under five minutes. [Start for free ->](https://my.webhookrelay.com/register) Free plan · No credit card required · 7-day money-back guarantee on paid plans --- --- title: How Lightning AI Ships Webhooks Team-Wide | WebhookRelay meta: "og: title": "How Lightning AI Ships Webhooks Team-Wide" description: How Lightning AI broadcasts Stripe webhooks to every developer's laptop and staging environment simultaneously — with zero setup per developer. url: https://webhookrelay.com/blog/lightning-ai-company-webhooks-setup.md file: /blog/lightning-ai-company-webhooks-setup.md --- ![Stripes](https://webhookrelay.com/blog/lightning-ai-company-webhooks-setup/images/stripes.svg) # **How Lightning AI uses WHR to ship webhooks to the entire team** How Lightning AI broadcasts Stripe webhooks to every developer's laptop and staging environment simultaneously — with zero setup per developer. When [building SaaS applications with Stripe](https://webhookrelay.com/blog/lightning-ai-company-webhooks-setup/blog/receiving-stripe-webhooks-localhost/), testing [webhook integrations locally](https://webhookrelay.com/blog/lightning-ai-company-webhooks-setup/blog/what-is-webhook/) is often a pain point. Traditional approaches involve ngrok tunnels, the [Stripe](https://stripe.com/) CLI, or mock data - all requiring manual setup for each developer. What if your entire team could receive real Stripe webhooks without any individual configuration? ## [The Challenge: Stripe Webhooks in Team Development](#the-challenge-stripe-webhooks-in-team-development) If you're building a SaaS product with Stripe, you know webhooks are critical for: - Updating subscription statuses - Tracking payment successes and failures - Managing customer credits - Handling plan upgrades and downgrades But getting webhooks working locally for a team is traditionally difficult: - Each developer needs to set up tunneling tools - Webhook URLs need constant updating in Stripe dashboard - Team members need Stripe dashboard access just to test - Debugging requires access to production webhook logs - New team members face lengthy setup processes ## [The Solution: Broadcast Webhooks to Everyone](#the-solution-broadcast-webhooks-to-everyone) Here's a better approach that we used for [Lightning AI](https://lightning.ai): configure Stripe to send all webhooks to Webhook Relay once, then broadcast them simultaneously to: - Every developer's local environment - Your staging server - Any other environments that need them The beauty? Each backend can decide whether to process or ignore webhooks based on whether it has that particular customer in its database. ## [How It Works](#how-it-works) 1. **Stripe sends all webhooks to Webhook Relay** - Configure this once in your Stripe dashboard 2. **Everyone runs the same docker-compose setup** - Web server, database, and Webhook Relay container 3. **Webhooks are broadcasted everywhere** - All environments receive all webhooks simultaneously 4. **Smart filtering happens in your backend** - Each instance checks if it has the customer by `customer_id` and processes or ignores accordingly 5. **Zero individual setup** - New team members just run `docker-compose up` and they're done ## [Setting Up Your Docker Compose](#setting-up-your-docker-compose) We will follow [Docker Compose setup instructions](https://webhookrelay.com/blog/lightning-ai-company-webhooks-setup/docs/installation/docker-compose/) and here's what the configuration looks like: ``` # Example docker-compose.yml file services: # Your main API service api: build: context: . dockerfile: Dockerfile ports: - "8080:8080" restart: always env_file: - .env # Database postgresql: image: "postgres:latest" ports: - "5432:5432" # Webhook Relay container forwarding to http://api:8080/webhooks webhookrelay: image: webhookrelay/webhookrelay:latest restart: always environment: # Authentication - RELAY_KEY=${RELAY_KEY} - RELAY_SECRET=${RELAY_SECRET} - BUCKETS=stripe-dev ``` That's it! The Webhook Relay container automatically connects to your configured bucket and starts forwarding webhooks to `http://api:8080/webhooks`. ## [Configuration Steps](#configuration-steps) ### [1. Create Your Webhook Relay Bucket](#_1-create-your-webhook-relay-bucket) 1. Sign up for a [Webhook Relay account](https://my.webhookrelay.com) 2. Create a new bucket called `stripe-dev` 3. Add multiple outputs: - Internal destination: `http://api:8080/webhooks` (for docker-compose) - Staging: `https://staging.yourapp.com/webhooks` - Any other environments ### [2. Configure Stripe](#_2-configure-stripe) 1. Go to your Stripe Dashboard → Developers → Webhooks 2. Add a webhook endpoint with your Webhook Relay input URL (e.g., `https://your-bucket.hooks.webhookrelay.com`) 3. Select the events you want to receive (or select all) 4. Copy your webhook signing secret for verification ### [3. Set Up Team Access](#_3-set-up-team-access) Add your `RELAY_KEY` and `RELAY_SECRET` to your team's shared configuration: 1. Generate API keys from your Webhook Relay [account settings](https://my.webhookrelay.com/tokens) 2. Add them to your `.env.example` file: ``` RELAY_KEY=your-key-here RELAY_SECRET=your-secret-here STRIPE_WEBHOOK_SECRET=whsec_... ``` 1. Team members copy `.env.example` to `.env` with the shared credentials ### [4. Implement Smart Webhook Filtering](#_4-implement-smart-webhook-filtering) Your backend should check whether it should process each webhook: ``` app.post('/webhooks', async (req, res) => { const sig = req.headers['stripe-signature']; let event; try { event = stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET); } catch (err) { return res.status(400).send(\`Webhook Error: ${err.message}\`); } // Extract customer ID from the event const customerId = event.data.object.customer; // Check if this customer exists in our local database const customer = await db.customers.findOne({ stripeCustomerId: customerId }); if (!customer) { // This webhook isn't for us - another environment will handle it console.log(\`Ignoring webhook for unknown customer: ${customerId}\`); return res.json({ received: true, processed: false }); } // Process the webhook for our customer switch (event.type) { case 'customer.subscription.updated': await handleSubscriptionUpdate(customer, event.data.object); break; case 'invoice.payment_succeeded': await handlePaymentSuccess(customer, event.data.object); break; case 'customer.subscription.deleted': await handleSubscriptionCancellation(customer, event.data.object); break; // ... handle other events } res.json({ received: true, processed: true }); }); ``` ## [The Benefits](#the-benefits) ### [1. Zero Setup for New Team Members](#_1-zero-setup-for-new-team-members) New developers just need to: ``` git clone your-repo cp .env.example .env docker-compose up ``` That's it! They're immediately receiving real Stripe webhooks. ### [2. No Stripe Dashboard Access Required](#_2-no-stripe-dashboard-access-required) Team members don't need access to your Stripe dashboard to: - Test webhook integrations - Debug webhook issues - Develop subscription features - Verify payment flows ### [3. Built-in Debugging](#_3-built-in-debugging) Webhook Relay provides a dashboard where you can: - See all webhook requests and responses - Inspect headers and payloads - Replay individual webhooks - View error messages and status codes ![Webhook Relay Dashboard debugging](https://webhookrelay.com/blog/lightning-ai-company-webhooks-setup/images/blog/lightning-webhooks-setup/debugging.png) Team members can debug their webhook handling without bothering DevOps or needing production access. ### [4. Test with Real Data](#_4-test-with-real-data) Instead of mocking Stripe webhooks, your team works with: - Real webhook payloads - Actual Stripe event structures - Production-like timing and sequences - Real edge cases you might not think to mock ### [5. Multiple Environments Simultaneously](#_5-multiple-environments-simultaneously) ![Forwarding to local and staging](https://webhookrelay.com/blog/lightning-ai-company-webhooks-setup/images/blog/lightning-webhooks-setup/local-and-staging.png) The same webhook gets delivered to: - Your local machine - Your coworker's local machine - The staging server - The QA environment Each processes only the webhooks relevant to its data. ## [Real-World Example](#real-world-example) Let's say you're testing a subscription upgrade: 1. You use Stripe's test mode to upgrade a test customer 2. Stripe sends `customer.subscription.updated` webhook 3. Webhook Relay broadcasts it to all environments 4. Your local database has this test customer → processes the webhook → updates the subscription 5. Your coworker's local database doesn't have this customer → ignores it 6. Staging has this customer → also processes it 7. Production (different bucket) isn't affected Everyone can test independently with their own test customers, all receiving real webhooks simultaneously. ## [Advanced: Environment-Specific Buckets](#advanced-environment-specific-buckets) For even better isolation, you can create separate buckets: - `stripe-production` - Only production server listens - `stripe-staging` - Staging + all developers - `stripe-feature-x` - Only developers working on feature X This gives you fine-grained control over webhook distribution. ## [Troubleshooting](#troubleshooting) ### [Webhooks not arriving?](#webhooks-not-arriving) - Check your Webhook Relay dashboard to confirm webhooks are being received - Verify the bucket name in your docker-compose matches your configuration - Ensure your `RELAY_KEY` and `RELAY_SECRET` are correct - Check the logs of webhookrelay container to see if it's receiving webhooks ### [Webhooks failing verification?](#webhooks-failing-verification) Make sure you're using the correct `STRIPE_WEBHOOK_SECRET` for webhook signature verification. This should match the secret shown in your Stripe dashboard for the webhook endpoint. ## [Conclusion](#conclusion) By using Webhook Relay with Docker Compose, you can: ✅ Configure Stripe webhooks once, use everywhere ✅ Onboard new developers in minutes ✅ Test with real webhook data locally ✅ Debug without Stripe dashboard access ✅ Support multiple environments simultaneously This approach scales from solo developers to large teams, and from local development to complex staging environments. The smart filtering pattern ensures each environment only processes webhooks relevant to its data, while the broadcast pattern ensures everyone has access to real webhook events. Ready to simplify your Stripe webhook development? [Sign up for Webhook Relay](https://my.webhookrelay.com/register) and get your team up and running in minutes. --- --- title: Automatically transform webhook payloads | WebhookRelay meta: "og: title": "Automatically transform webhook payloads" description: Automatically transform webhook payloads using AI in Webhook Relay. Step-by-step guide to convert webhook data between different formats without coding. url: https://webhookrelay.com/blog/auto-transform-webhook.md file: /blog/auto-transform-webhook.md --- ![Stripes](https://webhookrelay.com/blog/auto-transform-webhook/images/stripes.svg) # **Automatically transform webhook payloads** Automatically transform webhook payloads using AI in Webhook Relay. Step-by-step guide to convert webhook data between different formats without coding. In Webhook Relay you can automatically transform webhook payloads using [functions](https://webhookrelay.com/blog/auto-transform-webhook/docs/webhooks/functions/) that is basically a Lua code snippet to manipulate the payload. While powerful, it requires you to write the code yourself. Now, we are introducing a new feature that allows you to automatically transform webhook payloads using AI. ## [How it works](#how-it-works) You will need to provide two examples: - Input payload (that you want to transform) - Output payload (that you want to receive) We will use this to automatically generate the transformation function that will run on every webhook. ## [How to use it](#how-to-use-it) Go to the [magic transform](https://my.webhookrelay.com/new-transform) page. ![magic transform](https://webhookrelay.com/blog/auto-transform-webhook/images/blog/auto-transform-webhook/cover.png) Now, you will need to provide input and output payload samples. For this example we will use a simple JSON to demonstrate how it works. You can view more complex examples within the dashboard as well. ## [Do's and Don'ts](#dos-and-donts) The key thing to remember is that the input and output payload values should match. Input sample: ``` { "name": "John", "surname": "Doe", "age": 30, "city": "London" } ``` Output sample (good): ``` { "data_type": "person", "person": { "name": "John", "surname": "Doe", "age": 30, "city": "London" } } ``` Output sample (bad): ``` { "name": "Larry", "surname": "Sheet" } ``` ## [Try it out](#try-it-out) Let's add the samples and click "generate function": ![viewing generated function](https://webhookrelay.com/blog/auto-transform-webhook/images/blog/auto-transform-webhook/transform.png) Once generated, we can click continue and enter the destination URL. Feel free to grab a new testing endpoint from [https://bin.webhookrelay.com/](https://bin.webhookrelay.com/). You can either click "send a test request" or use `curl` to ping the endpoint ![send test request](https://webhookrelay.com/blog/auto-transform-webhook/images/blog/auto-transform-webhook/send-test-request.png) On the receiving end, you should see the transformed payload: ![received webhook](https://webhookrelay.com/blog/auto-transform-webhook/images/blog/auto-transform-webhook/received-webhook.png) It means we have successfully transformed the payload from our initial payload to the new format. ## [Troubleshooting](#troubleshooting) If the function generation is failing, try to check the following: - Input and output payloads should have matching values (names, types, etc.) - Input and output payloads should be valid JSON. We will introduce support for XML, form-data, and other formats in the future. If you have a use case for it, let us know. You can still use the function editor for that. - Try different examples if one doesn't work. You can also remove any irrelevant values from JSON structures. --- --- title: Airtable integrations: inserting rows | WebhookRelay meta: "og: title": "Airtable integrations: inserting rows" description: How to setup Airtable on setting up HTML contact form with Airtable code webhook integration url: https://webhookrelay.com/blog/airtable-integrations.md file: /blog/airtable-integrations.md --- ![Stripes](https://webhookrelay.com/blog/airtable-integrations/images/stripes.svg) # **Airtable integrations: inserting rows** How to setup Airtable on setting up HTML contact form with Airtable code webhook integration In this short article we will see how to integrate Airtable. For the first example we will receive an HTML form from a website and add a row to Airtable. This can be used for: You can use this form to: - Allow users to contact you - A way for users to submit bug reports - Collect user feedback - Waitlist signups for your product ![contact form on https://synpse.net/contact/](https://webhookrelay.com/blog/airtable-integrations/images/blog/contact-form-to-webhook/contact-form.png) _Example form on [https://synpse.net/contact/](https://synpse.net/contact/)_ ## [Prerequisites](#prerequisites) - [Webhook Relay account](https://my.webhookrelay.com). - [Airtable](https://airtable.com//) account with a workspace. - Static website of your choice. We are using websites hosted both on GitHub and Cloudflare pages, however with this setup it doesn't matter where the website is hosted. ## [Prepare Airtable](#prepare-airtable) In order to start adding rows to Airtable you will need to prepare few things: 1. Create a new base 2. Find your base ID and table name. To do this, check your URL when you are in the table view. It should look something like this: `https://airtable.com/appXXXX/tblXXXX/viwtXXXX`. The first part is your base ID and the second part is your table name. From here, `appXXXX` is the base ID and `tblXXXX` is the table ID. ## [Create forwarding configuration](#create-forwarding-configuration) You will need to create forwarding config to a public destination here [https://my.webhookrelay.com/new-public-destination](https://my.webhookrelay.com/new-public-destination). The URL will be `https://api.airtable.com/v0/appXXXX/tblXXXX`. > 🚨 Don't just copy/paste the URL from the browser! they are not the same as your browser uses `https://airtable.com/appXXXX/tblXXXX/viwtXXXX` but we need to send webhooks to `https://api.airtable.com/v0/appXXXX/tblXXXX` 🚨 ![airtable bucket](https://webhookrelay.com/blog/airtable-integrations/images/blog/airtable-webhook/airtable-bucket.png) Don't try to send requests to this yet as we will need to set up an Airtable webhook integration first. ## [Setup Airtable webhook integration](#setup-airtable-webhook-integration) Next step is to transform the incoming HTML form request into a webhook that will add or create a new record in your Airtable table: 1. Go to the [Functions](https://my.webhookrelay.com/functions) page 2. Create a new function and copy paste the code into the composer: ``` local json = require("json") local time = require("time") if r.RequestMethod == "POST" then -- Only POST requests allowed else r:StopForwarding() return end -- Taking fields from the HTML form and -- using them to prepare Airtable webhook local encoded_payload = { records={ { fields= { Email= r.RequestFormData.email[1], Name = r.RequestFormData.name[1], Added= time.format(time.unix(), "2006-01-02", "UTC") , Message = r.RequestFormData.message[1], } } } } -- Encoding payload to JSON local encoded_payload, err = json.encode(encoded_payload) if err then error(err) end r:SetRequestHeader("Authorization", "Bearer " .. cfg:GetValue("SECRET_API_TOKEN")) r:SetRequestHeader("Content-Type", "application/json") r:SetRequestBody(encoded_payload) r:SetRequestMethod("POST") ``` Once added: 1. Get your personal access token from here [https://airtable.com/create/tokens](https://airtable.com/create/tokens). Give it a name and ensure it has `data.records:write` **scope** and set **Access** to the base you want to add records to. 2. Open `CONFIG VARIABLES` tab of the function and create a new variable `SECRET_API_TOKEN` with the value of your personal access token: ![html form](https://webhookrelay.com/blog/airtable-integrations/images/blog/airtable-webhook/airtable-token-in-whr.png) ## [Setting up HTML form](#setting-up-html-form) We will use a simple HTML script. Replace the `https://XXXXXX.hooks.webhookrelay.com` URL with your own Webhook Relay input endpoint. You can find it in the Bucket details: ```

``` I have made a simple CodePen example here [https://codepen.io/defiant77/pen/gOQeRMm](https://codepen.io/defiant77/pen/gOQeRMm) which you can open and edit the webhooks URL with the one from your bucket. ![html form](https://webhookrelay.com/blog/airtable-integrations/images/blog/airtable-webhook/form.png) ## [Testing it out](#testing-it-out) Fill in the form and click submit. In your Airtable you should be able to see the new record. If you can't, check out webhook logs in Webhook Relay's bucket details. ## [Troubleshooting](#troubleshooting) There are very few moving parts here but if something doesn't work, things to check: - Form `action` URL is pointing at the bucket that you have created. - Function is attached to the output. It's required to have it as it needs to do the transformation. - The destination URL should be pointing at the correct base ID and table ID in Airtable. - Personal access token needs to have permissions to write (`data.records:write`) and access is allowed to the base that you are working with. That's it! 😃 You can also make a very similar integration for Google Sheets as the process is the same - using webhook as an API call. --- --- title: GKE Control-Plane Failure: May 2022 Outage | WebhookRelay meta: "og: title": "GKE Control-Plane Failure: May 2022 Outage" description: An RCA on how the managed GKE control-plane failure brought down the platform url: https://webhookrelay.com/blog/may-10th-outage-gke-controlplane.md file: /blog/may-10th-outage-gke-controlplane.md --- ![Stripes](https://webhookrelay.com/blog/may-10th-outage-gke-controlplane/images/stripes.svg) # **Managed GKE control-plane failure resulting in platform outage on 10th May, 2022** An RCA on how the managed GKE control-plane failure brought down the platform On May 10th, 2022, Webhook Relay suffered a major outage due to an underlying [Google Cloud GKE](https://cloud.google.com/kubernetes-engine) container platform failure which acts as our hosting provider for our main region where controllers and backend APIs reside. We have been using GKE for a long time (since 2017) and most of the time it just works. A rough timeline of the outage (all times EST): - 12:00PM Healthcheck system flares up, backend APIs, frontend and tunnels in EU region are down. We begin mitigating the outage. - 12:10PM All information in the Kubernetes cluster seems inconsistent, looks like either the nodes are down or in an upgrade process. We have seen this before a few years ago with an automated maintenance upgrade on GKE going bad, however this is not a maintenance window. - 12:20PM Node pool recycling is taking a long time, the cluster is timing out, operations fail multiple times until they succeed, the root cause is still unclear. - 12:40PM We have found out the root cause, it's the API server certificates. We initialize manual certificate rotation. - 13:20PM During the certificate rotation, nodes should be recreated, however nothing happens in our GKE cluster, we recreate node-pools manually again. - 13:50PM Workloads start running, however service account based authentication inside the cluster is failing. Backend services fail to connect to the database, object storage and pubsub services aren't functional. - 14:30PM By now we are already duplicating the main cluster services into a newly created cluster. The new cluster is also having hick-ups, control-plane becomes unresponsive every few minutes. - 15:00PM It is not possible to connect to the Clickhouse database where we store the webhook logs as the port forward commands fail. Logs do not work as well through the Kubernetes API. - 15:30PM Main GKE cluster is still in a crippled state, certificate rotation, while succeeds, doesn't seem to be effective. GKE console on GCP works, it is possible to see logs and at least update the deployments. - 16:00PM Most of the services are already running in a duplicated cluster, however since we rely on load balancers and PVCs that are still in a primary cluster, we need to detach them from there first. - 16:50PM We were able to delete Kubernetes service records from the primary cluster, this allowed us to attach them to the new cluster. - 17:10PM New backend is running, however webhook routes are going to a fresh disk. - 17:30PM StatefulSet detached as well, switched all writes to the main disk. - 18:30PM Temporary Clickhouse instance data exported and reinserted into the main instance. ## [The initial alert](#the-initial-alert) After receiving the first healthcheck notification that the service went down, we thought that potentially it's just that the request got interrupted on the client side or hit a server that got evicted due to a memory or CPU utilization issue, we knew that these errors are transient and the recovery should be quick and automatic. However, after checking out the main dashboard and seeing it offline, we immediately started mitigating the outage. ## [Troubleshooting](#troubleshooting) The main problem with this outage was the lack of information provided by the GKE console. It didn't look completely right but it also didn't look wrong. What did work: - Ability to see, create and manage node pools. Nodes were all going into the healthy state and start the workloads. However, all operations were very slow. - Ability to edit deployments, view logs - Ability to upgrade control plane version What didn't work: - `kubectl` was able to list and retrieve objects, however it couldn't view logs or do port forwarding - `kubectl` couldn't update objects So the cluster was running, the workloads were running, however some workload info was being returned definitely as stale. Running workloads were having trouble accessing various helper services that Google Cloud provides. It took as awhile to pin point the problem with the Kubernetes API server certificates. In GKE on older clusters they used to give 5 year certificates (now the clusters come with 30 year expiration) and the certificate rotation never happened. You can also manually start the rotation with: ``` gcloud container clusters update --zone europe-west1-d --start-credential-rotation ``` But unfortunately this operation while succeeding - didn't actually do anything. We found a useful command which can show the validity of your GKE cluster: ``` gcloud container clusters describe --zone europe-north1-a \ --format "value(masterAuth.clusterCaCertificate)" \ | base64 --decode \ | openssl x509 -text \ | grep Validity -A 2 ``` ## [Recovery](#recovery) GKE has multiple availability zones that increase your application's reliability in case one of the zones fail. However, in this case that didn't matter :) Manual certificate rotation didn't help. Upgrading control-plane also didn't help. The pods were running but all the backing services were not accessible. Backend services could not connect to Cloud SQL, PubSub or GCS. Once we noticed the authentication errors, we started moving services to the cluster. ## [The length of the downtime](#the-length-of-the-downtime) It would have been much shorter downtime duration if we were aware that the managed GKE cluster is totalled. It always seemed that it's about to start working. If we had the knowledge about the real cluster state, we would have made different choices as while the complete migration to a new cluster is not without its issues (moving persistent disks, detaching load balancers), it would have been a lot quicker. ## [Lessons learnt](#lessons-learnt) The main lesson here for us was that it's important to time-box the recovery of an existing infrastructure. While throughout the outage it looks like services are about to start working, we should have pulled the plug on the cluster much sooner and start from scratch with a new one. The sunk-cost fallacy phenomenon made us reluctant to ditch the salvage efforts which would have been the right call. As part of the work during the outage we have improved our deployment manifests to be able to quickly switch between clusters without a complicated persistent disk data migration. --- --- title: Static IPs for outgoing webhooks | WebhookRelay meta: "og: title": "Static IPs for outgoing webhooks" description: How to setup static IPs for webhook calls to enable whitelisting url: https://webhookrelay.com/blog/static-ip.md file: /blog/static-ip.md --- ![Stripes](https://webhookrelay.com/blog/static-ip/images/stripes.svg) # **Static IPs for outgoing webhooks** How to setup static IPs for webhook calls to enable whitelisting We now offer static IP addresses for your webhook calls. This helps you whitelist IPs when connecting to other services. ## [Why use static IPs?](#why-use-static-ips) - Improved security: Only allow connections from known IPs - Easier setup: No need to update IP lists if your server changes - More reliable: Some services require static IPs ## [How Webhook Relay implements static IPs](#how-webhook-relay-implements-static-ips) Webhook Relay uses a static IP address `5.161.20.156` for outbound webhook requests. When you make a new Bucket in Webhook Relay, you can turn on "Static IP". All webhooks in that Bucket will then use the same IP address. This makes it easier to whitelist your IP with other services. ## [How to use](#how-to-use) To use a static IP: 1. Create a new Bucket 2. Turn on the "Static IP" option 3. Set up your webhook as normal Here's what it looks like: ![static IP](https://webhookrelay.com/blog/static-ip/images/blog/static-ip/config.png) ## [Try it out](#try-it-out) For our example, we will set destination to `https://ifconfig.me`. Then if you make a new request to your Bucket: ``` curl https://dhzxfih3wkuyxfo0jbgv4k.hooks.webhookrelay.com ``` Then we can see that IP address `5.161.20.156` is the same as the one in the Bucket settings: ![static IP is set](https://webhookrelay.com/blog/static-ip/images/blog/static-ip/config.png) ## [That's it!](#thats-it) That's it! You can now use static IPs for your webhook calls to enable whitelisting in the receiving infrastructure. ## [Troubleshooting](#troubleshooting) - If the static IP isn't working, check that you enabled it in your Bucket settings - Make sure you're using the correct Bucket's input URL in your requests - Some services may need extra steps to whitelist IPs - check their docs ## [Learn more](#learn-more) - [What are webhooks?](https://webhookrelay.com/blog/static-ip/blog/what-is-webhook/) - [Security best practices for webhooks](https://webhookrelay.com/blog/static-ip/blog/webhook-security/) - [Using webhooks with popular services](https://webhookrelay.com/blog/static-ip/docs/webhooks/functions/) --- --- title: Run Dockerized Jenkins CI With Webhooks | WebhookRelay meta: "og: title": "Run Dockerized Jenkins CI With Webhooks" description: A quick tutorial on how to setup a Jenkins CI server using Docker, Synpse and Webhook Relay to have remote SSH access and secure webhooks url: https://webhookrelay.com/blog/install-jenkins-ci-docker.md file: /blog/install-jenkins-ci-docker.md --- ![Stripes](https://webhookrelay.com/blog/install-jenkins-ci-docker/images/stripes.svg) # **How to install and run a dockerized Jenkins CI with webhook support** A quick tutorial on how to setup a Jenkins CI server using Docker, Synpse and Webhook Relay to have remote SSH access and secure webhooks [Jenkins](https://jenkins.io/) is an extremely popular automation server that can run tests, build and publish software, or perform pretty much any other user-defined action. [Docker](https://www.docker.com/), on the other hand, is quite a focused tool; it's used to package software into containers and run them. This makes Jenkins and Docker a good pair when testing, shipping and deploying things. In this tutorial, you’ll see how easy it is to install and run a modern, all-in-one dockerized Jenkins with [Synpse](https://synpse.net) which provides: - Rock-solid deployment with persistent storage - Webhook delivery to the server to trigger build jobs without public IP or domain - Secure remote SSH access to the server without exposing it to the internet We will split this article into several sections - installation, administration, and webhook configuration. Let’s get started. ![Jenkins with Synpse and WHR](https://webhookrelay.com/blog/install-jenkins-ci-docker/images/blog/install-jenkins-ci-docker/cover.png) ## [Prerequisites](#prerequisites) Webhook Relay and Synpse have paid tiers with increased quotas and support plans; however a free tier should be enough for setting this up. - [Webhook Relay account](https://my.webhookrelay.com) - free tier should be enough for setting up homelab or testing - [Synpse account](https://cloud.synpse.net) - remote device management, free up to 5 devices (definitely enough for Jenkins!) - Linux server, ideally Ubuntu, however other distros should work as well - [Docker](https://docker.com) - installed on the server ## [Installation](#installation) First, install the Synpse agent on your server. You can view installation instructions here [https://docs.synpse.net/agent/install/linux-docker](https://docs.synpse.net/agent/install/linux-docker). This will provide us with lightweight deployment capabilities. Once installed, add the label `type: controller` to that device in your Synpse dashboard. ![Synpse devices](https://webhookrelay.com/blog/install-jenkins-ci-docker/images/blog/install-jenkins-ci-docker/synpse-devices.png) Next, let's create an application that will run on the server: ``` name: jenkins description: CI/CD server scheduling: type: Conditional selectors: type: controller spec: containers: - name: jenkins image: jenkins/jenkins:lts user: root privileged: true ports: - 8080:8080 - 50000:50000 volumes: # Persistent volumes - /data/jenkins-compose:/var/jenkins_home - /var/run/docker.sock:/var/run/docker.sock ``` This will download the image and start the container. Once it shows up as ready, open the `http://device-ip:8080` address. You will then require an initial admin password, found through Synpse inbuilt web SSH terminal: ![getting the token](https://webhookrelay.com/blog/install-jenkins-ci-docker/images/blog/install-jenkins-ci-docker/jenkins-token.gif) Enter your initial admin password and proceed with the plugin installation: ![getting the token](https://webhookrelay.com/blog/install-jenkins-ci-docker/images/blog/install-jenkins-ci-docker/plugin-install.png) > You should also setup [Jenkins agents](https://www.jenkins.io/doc/book/using/using-agents/) to do the heavy work as they can just be additional containers either in the same application spec or separate applications that Jenkins server connects to but we will explore that route in a separate blog post. ## [Webhooks Without Public IP or Domain](#webhooks-without-public-ip-or-domain) It's important to be able to receive webhooks without exposing our Jenkins server to the internet. For this, we will need to deploy a container next to the Jenkins server which will help with request forwarding. Let's start by getting the tokens from [https://my.webhookrelay.com/tokens](https://my.webhookrelay.com/tokens) and creating two secrets in Synpse named **webhookrelayKey** and **webhookrelaySecret** that contain your authentication tokens. Then, go to [https://my.webhookrelay.com/buckets](https://my.webhookrelay.com/buckets) page and create a bucket named **jenkins**. Now, we can add the new container to the Jenkins server app: ``` name: jenkins description: CI/CD server scheduling: type: Conditional selectors: type: controller spec: containers: - name: jenkins image: jenkins/jenkins:lts user: root privileged: true ports: - 8080:8080 - 50000:50000 volumes: # Persistent volumes - /data/jenkins-compose:/var/jenkins_home - /var/run/docker.sock:/var/run/docker.sock # Webhook Relay forwarding container. This container allows # us to receive the webhooks that are hitting public server endpoints - name: webhookrelay image: webhookrelay/webhookrelayd:latest env: - name: RELAY_KEY fromSecret: webhookrelayKey - name: RELAY_SECRET fromSecret: webhookrelaySecret - name: BUCKETS value: jenkins ``` Click save and after a few seconds we should see two containers running: ![whr container](https://webhookrelay.com/blog/install-jenkins-ci-docker/images/blog/install-jenkins-ci-docker/whr-container.png) #### [Configuring Jenkins Plugin](#configuring-jenkins-plugin) The easiest way to start receiving GitHub webhooks is by using this plugin [https://plugins.jenkins.io/github](https://plugins.jenkins.io/github). To install: - Go to your Jenkins plugin manager - Find and install "GitHub plugin" (at the time of writing - current version was 1.27.0) - Once it installed, we will need to configure it: ![manage jenkins](https://webhookrelay.com/blog/install-jenkins-ci-docker/images/blog/jenkins-guide/jenkins-manage.png) Add default GitHub server (don't bother adding credentials as we are using public repo anyway): ![jenkins add gh](https://webhookrelay.com/blog/install-jenkins-ci-docker/images/blog/jenkins-guide/jenkins-add-gh.png) #### [Configuring Jenkins Job](#configuring-jenkins-job) When you want Jenkins to do something - create a job. In this case, we will be using **Freestyle project**: ![jenkins create job](https://webhookrelay.com/blog/install-jenkins-ci-docker/images/blog/jenkins-guide/jenkins-new-job.png) We have to configure several sections here - **Source Code Management** and **Build Triggers**. First, set repository (in this case it's my demo app repo [repository](https://github.com/rusenask/jenkins-whr-demo)): ![jenkins create job](https://webhookrelay.com/blog/install-jenkins-ci-docker/images/blog/jenkins-guide/jenkins-code-management.png) Next step is setting a build trigger to **GitHub hook trigger for GITScm polling**: ![jenkins build trigger](https://webhookrelay.com/blog/install-jenkins-ci-docker/images/blog/jenkins-guide/jenkins-build-triggers.png) This means that once the Jenkins receives a webhook, it can identify which repo is changed and thus triggers a pull and job execution. #### [Configure Webhook Relay Bucket](#configure-webhook-relay-bucket) Once things are running, go back to your bucket details in Webhook Relay and add Jenkins container as the destination. It should be: - Name: `jenkins` - Destination `http://jenkins:8080/github-webhook/` - Type: `internal` ![bucket destination](https://webhookrelay.com/blog/install-jenkins-ci-docker/images/blog/install-jenkins-ci-docker/bucket-destination.png) > Port always needs to match the port on which the application is running in a container. For example, if you do a mapping of 8888:8080 for Jenkins server, destination should still be on port 8080 as that is what's available internally. #### [Configure GitHub](#configure-github) Go to your repository settings and add the input endpoint URL from your Webhook Relay bucket: ![github config](https://webhookrelay.com/blog/install-jenkins-ci-docker/images/blog/install-jenkins-ci-docker/github-config.png) ## [Push to Build!](#push-to-build) Once you push to your repository, you should see few things: 1. In Webhook Relay dashboard a new webhook received and forwarded 2. A new build in Jenkins dashboard ## [Let's wrap up](#lets-wrap-up) In this tutorial, we deployed the main Jenkins server as a Docker container with persistent volumes. We also configured GitHub webhooks to trigger builds without us requiring to have a public IP or configure firewalls. In our upcoming blog post, we will explore ways of connecting more agents to the server. --- --- title: Changes to our prices for new customers | WebhookRelay meta: "og: title": "Changes to our prices for new customers" description: As of 1st of May 2021, new WHR subscribers will be subject to new prices. url: https://webhookrelay.com/blog/pricing-changes.md file: /blog/pricing-changes.md --- ![Stripes](https://webhookrelay.com/blog/pricing-changes/images/stripes.svg) # **Changes to our prices for new customers** As of 1st of May 2021, new WHR subscribers will be subject to new prices. Dear Members, As of 1st May 2021, we will be changing our subscription prices to incorporate an increase across all paid plans for new customers. This modest adjustment­­ — our first since we started out in 2017 — ­­equates to an additional $0.49 for Basic, $20 for Standard, $30 for Business and $100 for Pro, per month respectively. Our new prices will be as follows: | Plans | Old Pricing | New Pricing | | --- | --- | --- | | Basic | $4.50 | $4.99 | | Standard | $19.99 | $39.99 | | Business | $49.99 | $79.99 | | Pro | $149.99 | $249.99 | The circumstances prompting this change includes; - Features such as [Functions](https://webhookrelay.com/blog/pricing-changes/docs/webhooks/functions/), [Kubernetes Operator](https://webhookrelay.com/blog/pricing-changes/docs/installation/kubernetes/) and websocket support & various integrations with 3rd party tools, - Increasing Google cloud fees, and - Around-the-clock tech support. We, at WHR, feel grateful to be able to rely on the support of our existing customers. And as a thank you, we will exclude them from any imminent price increase. By capping the subscription price for our existing customers and by continuing to offer WHR’s free starter version for our new users, ­­we seek to keep WHR affordable. > Please note, any customers signing up for our services before 1st of May 2021 will continue to pay our old prices. --- --- title: Self-hosted business intelligence with Metabase meta: "og: title": "Self-hosted business intelligence with Metabase" description: Setting up self-hosted Metabase on-prem url: https://webhookrelay.com/blog/setting-up-selfhosted-metabase.md file: /blog/setting-up-selfhosted-metabase.md --- ![Stripes](https://webhookrelay.com/blog/setting-up-selfhosted-metabase/images/stripes.svg) # **Self-hosted business intelligence with Metabase** Setting up self-hosted Metabase on-prem ![Metabase](https://webhookrelay.com/blog/setting-up-selfhosted-metabase/images/blog/metabase/metabase_synpse_whr.png) It is always useful to know how your business or projects are doing and for that, there are a bunch of tools available such as Excel spreadsheets, Google DataStudio, Apache Superset, etc. I personally am a fan of Metabase as it is the easiest to deploy and use. When paired with the right technologies, this setup becomes trivial to anything from a small organization to a big company. In this article, we will use a setup that works exactly the same way on both an Intel NUC (for some of my projects) and on a large VM that is managed by a VMware. ## [Prerequisites](#prerequisites) - [Webhook Relay account](https://webhookrelay.com) - will be used to expose the Metabase to the internet, so we can access it. - [Synpse account](https://synpse.net) - a lightweight and fantastic platform to manage and run software on your own hardware. Offers management of up to 5 devices for free. ## [1. Install Synpse into your server/machine](#_1-install-synpse-into-your-servermachine) Once you log into [Synpse Cloud](https://cloud.synpse.net), select your project and then head to the "Devices" page. From there you will be able to find the auto-generated command that you need to run on the device to add it to your project. There are multiple ways to do it however initially you can just SSH into the machine via local network. Once you run the command, after a few seconds (depends on your internet speed) you should see the magic happen and device appear in your "Devices" page in Synpse :) ## [2. Create a Webhook Relay tunnel for your Metabase app](#_2-create-a-webhook-relay-tunnel-for-your-metabase-app) Our Metabase deployment will need to be reachable from outside so we can actually view reports. For this, we are creating a Webhook Relay tunnel that will be established by a `webhookrelayd` container. Go to your [https://my.webhookrelay.com/tunnels](https://my.webhookrelay.com/tunnels) page and create a new tunnel with these details: - name: 'whr-metabase' (`webhookrelayd` agent will need to know which tunnel to use) - destination: ' [http://metabase:3000](http://metabase:3000)' (metabase is reachable using container's name) ## [3. Get your access token key and secret](#_3-get-your-access-token-key-and-secret) Head to the tokens page here [https://my.webhookrelay.com/tokens](https://my.webhookrelay.com/tokens) and create a new pair. Save the key and secret before closing the window. Go to the secrets page in Synpse and create both `relayKey` and `relaySecret` secrets: ![Create Synpse secrets](https://webhookrelay.com/blog/setting-up-selfhosted-metabase/images/blog/metabase/synpse_secret.png) ## [4. Deploy Metabase via Synpse](#_4-deploy-metabase-via-synpse) Last step is to create a Synpse application. ``` name: metabase description: metabase + WHR scheduling: type: Conditional selectors: type: controller spec: containers: - name: metabase image: metabase/metabase:latest volumes: - /data/metabase:/metabase-data env: - name: MB_DB_FILE value: /metabase-data/metabase.db - name: MB_REDIRECT_ALL_REQUESTS_TO_HTTPS value: "false" - name: relayd image: webhookrelay/webhookrelayd:1 args: - --mode - tunnel - -t - whr-metabase # <--- if you have chosen a different name for the tunnel, change it here too env: - name: RELAY_KEY fromSecret: relayKey - name: RELAY_SECRET fromSecret: relaySecret ``` Once deployed, use your tunnel URL to access it. You can also configure Google OAuth to make the login easier, however that's out of scope in this article. Enjoy! --- --- title: Kubernetes Access Through Tunnels — Case Study meta: "og: title": "Kubernetes Access Through Tunnels — Case Study" description: How Lithuania's transport operator uses Webhook Relay tunnels to reach private Kubernetes clusters with TLS pass-through and an ingress. url: https://webhookrelay.com/blog/tunnels-to-kubernetes.md file: /blog/tunnels-to-kubernetes.md --- ![Stripes](https://webhookrelay.com/blog/tunnels-to-kubernetes/images/stripes.svg) # **Providing access to Kubernetes through tunnels in one of the largest cities in Lithuania** How Lithuania's transport operator uses Webhook Relay tunnels to reach private Kubernetes clusters with TLS pass-through and an ingress. ## [The product](#the-product) The core tenet of the Webhook Relay service is receiving and processing HTTP requests. When used with bidirectional tunnels it's a **1:1** relationship that simply exposes the underlying service to the internet. Our client software can run on any machine (Windows, MacOS, Linux x86 and ARM) as well as Docker containers, Kubernetes deployments. When used with webhook forwarding, relationship becomes **N:N**, meaning that there can be multiple public endpoints that will be routing to multiple destinations that are either internal (private network) or public destinations. All HTTP requests can be transformed on arrival to our system or before getting dispatched. A single webhook can be transformed into a new request that is tailored for any API. ## [Webhook Relay masterplan](#webhook-relay-masterplan) - Remove all friction from exposing internal services to the internet for easy access. - Securely let traffic in for sensitive systems (CI/CD) using unidirectional forwarding. - Transform webhooks, when needed. ## [Kaunas public transport company](#kaunas-public-transport-company) ![Lithuania](https://webhookrelay.com/blog/tunnels-to-kubernetes/images/blog/lt-tunnels-to-kubernetes/bridge-lt.jpg) Kaunas is the second biggest city in Lithuania, with big plans for both its physical and digital infrastructure. The city has invested heavily into modernizing vehicles as well as rebuilding their software stack. The first step was to start offering e-tickets via mobile application payments - that’s how [Žiogas](https://kaunoziogas.lt/en/) was born. With the new mobile app and a modern, cloud-based backend running on GKE (Google Cloud Platform Kubernetes engine) was designed and deployed. With a lot of workloads running on Kubernetes, a need for staging and development environments was as high as ever. However, due to an existing infrastructure, getting load balancers to work with Kubernetes or even to get an access to Kubernetes API server for the kubectl from outside was not trivial. A decision was made to: - Use TLS-pass-through tunnels to access Kubernetes as the authentication and authorization is performed using certificates. - Deploy ingress controller that will provide access to the workload APIs and web dashboards. - CI/CD integration with GitHub via webhooks and an easy access to the build server itself using tunnels. ## [Use case: TLS-pass-through to enable access for kubectl](#use-case-tls-pass-through-to-enable-access-for-kubectl) Deploying a development/staging environment was simple, however to enable an easy access that doesn’t require users to SSH into a jump server and then downloading manifests wasn’t very simple. This is where Webhook Relay TLS-pass-through type tunnels came into play. We have launched a standalone webhookrelayd container on the same machine that could then open a direct tunnel to Kubernetes API server. Then, we only had to update the .kubeconfig to point to the public tunnel hostname instead of the internally running Kubernetes and we got the access. ## [Use case: Tunnel based ingress controller for the workloads](#use-case-tunnel-based-ingress-controller-for-the-workloads) Having access to Kubernetes via kubectl was great but to get access to the workloads was crucial as the product mostly consists of a back-office web portal for system administration and an end-user facing API. As this was a closed environment, services couldn’t be exposed directly to the internet for testing and development purposes. Hence, we have deployed Webhook Relay ingress controller that would let us achieve several goals: - HTTPs endpoints for our workloads without having to configure NAT, routing or getting a domain. - Easy access to any new workloads as there’s no shortage of custom subdomains. - Same principles of configuration in staging and production - just through ingress.yaml files. The only difference is ingress controller class. ## [Key results](#key-results) - A closed corporate environment without public access can now expose services through an ingress controller. - Kubernetes '_kubectl_' can reach the API server to create/view/manage deployments, services, etc. - CI/CD utilizing vast internal resources - Excellent integration with GitHub, enabling a modern CI pipeline. - Using tunnels to make private infrastructure feel like a cloud environment. ## [Conclusion](#conclusion) Webhook Relay allowed the team to speed up its development and testing process. Our client could finally utilize large servers for Kubernetes that would otherwise have cost thousands of dollars on a cloud environment. All this, while retaining an excellent user experience as in a cloud;easy access to Kubernetes and on-demand access to any existing or new workload. --- --- title: Ingesting Facebook webhooks (challenge & verification) meta: "og: title": "Ingesting Facebook webhooks (challenge & verification)" description: How to receive Facebook webhooks and do verification for challenge and token url: https://webhookrelay.com/blog/ingesting-facebook-webhooks.md file: /blog/ingesting-facebook-webhooks.md --- ![Stripes](https://webhookrelay.com/blog/ingesting-facebook-webhooks/images/stripes.svg) # **Ingesting Facebook webhooks (challenge & verification)** How to receive Facebook webhooks and do verification for challenge and token Facebook lets you receive information about various events via webhooks. Documentation can be found here [https://developers.facebook.com/docs/messenger-platform/getting-started/webhook-setup/](https://developers.facebook.com/docs/messenger-platform/getting-started/webhook-setup/). These webhooks are almost like any other webhooks except they also ask you to do the verification of the token (similarly to shared secret and HMAC in other platforms) but the slight difference comes when dealing with the challenge. In this short tutorial, I will demonstrate how to configure Facebook webhooks so Webhook Relay will solve the challenge for you. ![Facebook webhooks](https://webhookrelay.com/blog/ingesting-facebook-webhooks/images/blog/facebook-webhooks/facebook-webhooks.png) You can read more about the Messenger Platform here: [https://developers.facebook.com/docs/messenger-platform/introduction](https://developers.facebook.com/docs/messenger-platform/introduction). ## [Create a bucket in Webhook Relay](#create-a-bucket-in-webhook-relay) First, create a new Webhook Relay bucket. You can do that by visiting [buckets page](https://my.webhookrelay.com/buckets). ![Creating Webhook Relay bucket](https://webhookrelay.com/blog/ingesting-facebook-webhooks/images/blog/facebook-webhooks/create-bucket.png) Here, you will see your "Default public endpoint" that should look like `.hooks.webhookrelay.com`. ## [Configure webhooks endpoint on Facebook](#configure-webhooks-endpoint-on-facebook) ![Configuring Facebook webhooks](https://webhookrelay.com/blog/ingesting-facebook-webhooks/images/blog/facebook-webhooks/configure-fb.png) Click on subscribe and then add your Webhook Relay public endpoint: ![Adding Webhook Relay endpoint to Facebook](https://webhookrelay.com/blog/ingesting-facebook-webhooks/images/blog/facebook-webhooks/add-whr-to-fb.png) If you click on "Verify and Save" it will show you an error and looking at the request in the Webhook Relay dashboard, we can see that the query had several values `Query: hub.mode=subscribe&hub.challenge=1903260781&hub.verify_token=my-facebook-token`. We need to take the challenge and set it as a header. ## [Solving the Facebook webhook verification challenge](#solving-the-facebook-webhook-verification-challenge) The way to solve the Facebook verification challenge is to get the value from the URL query and set it to the response body. This can be achieved with the Webhook Relay Function. Go to Functions page [https://my.webhookrelay.com/functions](https://my.webhookrelay.com/functions) and create a new function called "facebook-verification". Copy paste the code below: ``` local mode = r.RequestQuery["hub.mode"] if mode == "subscribe" then r:SetResponseBody(r.RequestQuery["hub.challenge"]) end ``` Now, go back to the bucket that you have previously created, click on the Inputs and then "CONFIGURE" on your input: ![Setting up function](https://webhookrelay.com/blog/ingesting-facebook-webhooks/images/blog/facebook-webhooks/set-function.png) Now, if you try "Save and Verify" again from the Facebook webhooks page, it should pass. You can now try sending some "test" events from the platform: ![Sending test event](https://webhookrelay.com/blog/ingesting-facebook-webhooks/images/blog/facebook-webhooks/test-event.png) ## [Facebook webhook token authentication](#facebook-webhook-token-authentication) Facebook also sends a token that you have supplied when adding configuration. In my example, it was `my-facebook-token`. We can get this value too and verify in the Function: ``` local mode = r.RequestQuery["hub.mode"] -- Shared secret - replace with yours!! local my_shared_secret = "my-facebook-token" -- If mode is subscribe, validating it if (mode == "subscribe") then -- Getting verify token local token = r.RequestQuery["hub.verify_token"] -- Validating token if not(token == my_shared_secret) then r:SetResponseStatusCode(401) r:StopForwarding() return end -- Responding to challenge r:SetResponseBody(r.RequestQuery["hub.challenge"]) end ``` Update the function, remove Facebook subscription and then try again. That's it, you can now configure destinations either to your public or private application servers and start processing Facebook webhooks. --- --- title: Running Webhook Relay agent with Podman | WebhookRelay meta: "og: title": "Running Webhook Relay agent with Podman" description: A short guide how to run Webhook Relay agent with Podman url: https://webhookrelay.com/blog/webhookrelayd-with-podman.md file: /blog/webhookrelayd-with-podman.md --- ![Stripes](https://webhookrelay.com/blog/webhookrelayd-with-podman/images/stripes.svg) # **Running Webhook Relay agent with Podman** A short guide how to run Webhook Relay agent with Podman ![Webhooks](https://webhookrelay.com/blog/webhookrelayd-with-podman/images/blog/podman-webhookrelay/podman-webhookrelay.png) If you are familiar with Docker, you probably have also heard about [Podman](https://docs.podman.io). Podman doesn't try to do many things that Docker does, it's a daemonless tool that provides an easy way to run, find and build [OCI](https://opencontainers.org/) containers. We are happy to include documentation on how to run Webhook Relay tunnelling agent with Podman and also to announce that we are now providing agent images that are built on top of Redhat's [Universal Base Image (UBI)](https://access.redhat.com/articles/4238681). ## [Getting started](#getting-started) First, get a token key & secret pair from [https://my.webhookrelay.com/tokens](https://my.webhookrelay.com/tokens). Once you got it, to launch a container via Podman we will be using our ubi8 based image **webhookrelay/webhookrelayd-ubi8:latest**. Podman has very similar run syntax to Docker's so you will probably feel right at home if you have used Docker before: ``` podman run -it docker.io/library/busybox ``` You can find "podman run" command docs [here](https://docs.podman.io/en/latest/markdown/podman-run.1.html). From that page let's look at [environment variable](https://docs.podman.io/en/latest/markdown/podman-run.1.html#environment) configuration as we will need to supply access token and secret: ``` podman run -d --env RELAY_KEY=your-token-key --env RELAY_SECRET=your-token-secret --env BUCKETS=your-bucket-name --network host webhookrelay/webhookrelayd-ubi8 ``` Here we also added: - **--env BUCKETS=your-bucket-name** - a comma separated list of buckets to forward - **--env RELAY_KEY=your-access-token-key** - access token key (get key & secret pair from the [tokens page](https://my.webhookrelay.com/tokens)) - **--env RELAY_SECRET=your-access-token-secret** - access token secret - **-d** - 'detach', this allows container to be started in a background - **--network host** - this is only needed for localhost (as our test server was running on the same machine). In a normal setup Webhook Relay agent would be started anywhere in the internal network and forward webhooks based on either internal DNS or IP addresses, therefore this flag wouldn't be needed. Also, if it's sending requests to some other container avoid **host** network. Now, we can check whether the container is running: ``` podman ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES docker.io/webhookrelay/webhookrelayd-ubi8:latest About a minute ago Up About a minute ago gallant_bardeen ``` To view container logs: ``` podman logs 2020-05-31 21:26:43.267 INFO using standard transport... 2020-05-31 21:26:43.356 INFO webhook relay ready... {"host": "my.webhookrelay.com:8080", "buckets": ["podman-test"]} ``` That's it, to remove container once you are done: ``` podman rm -f ``` --- --- title: CDN types and setting them up (Vue, React) | WebhookRelay meta: "og: title": "CDN types and setting them up (Vue, React)" description: CDN (content delivery network) types and how to set one up (Vue, React) url: https://webhookrelay.com/blog/cdn-types-and-setup.md file: /blog/cdn-types-and-setup.md --- ![Stripes](https://webhookrelay.com/blog/cdn-types-and-setup/images/stripes.svg) # **CDN types and setting them up (Vue, React)** CDN (content delivery network) types and how to set one up (Vue, React) ![CDN](https://webhookrelay.com/blog/cdn-types-and-setup/images/blog/cdn/cover.webp) What is CDN? Cloudflare has a nice explanation here: [https://www.cloudflare.com/learning/cdn/what-is-a-cdn/](https://www.cloudflare.com/learning/cdn/what-is-a-cdn/) In short: > A content delivery network (CDN) refers to a geographically distributed group of servers that work together to provide fast delivery of Internet content. In this article we will have a quick look into several CDN types and potentials pros/cons that you might encounter when setting one up yourself. Cloudflare is one of the best CDNs out there and we are using it for our landing pages and numerous other projects. It's a great DNS configuration service too that provides rich APIs. However, it's good to understand what other types of CDNs are out there and which suits you best. ## [CDN types](#cdn-types) All CDNs have different pros and cons and all solutions are trying to achieve the same thing: load content faster. ### [Reverse proxies with caching](#reverse-proxies-with-caching) Some of the CDN types you will encounter in the wild: - [Cloudflare](https://cloudflare.com/) type proxies that forward all incoming traffic to your origin servers and cache as much as possible. Pros: - Ease of use. Your application doesn't have to be aware of the CDN itself. If you are using Cloudflare as a DNS provider you just click on the button and their servers start intercepting all traffic and caching it. On top of that, they offer a bunch of other useful services like firewalls, "page rules" that can redirect Cons: - Can be caching too much (you don't see updates once you push because index.html is also cached). - Since they are terminating connections, if they go down together with your DNS control, it becomes harder to recover. - Lack of control from your side and potential security implication of allowing 3rd party to terminate TLS for you. ### [Push CDN](#push-cdn) ![Push CDN](https://webhookrelay.com/blog/cdn-types-and-setup/images/blog/cdn/push-cdn.png) **Push CDN** is a setup where you upload your assets to a server (or a group of servers). An example of such CDN is Google [Cloud CDN](https://cloud.google.com/cdn/). In this setup, you will have to create a load balancer and a storage bucket and upload your content assets as part of the CI/CD pipeline where you build your frontend app. In this setup, you will need to create a new domain such as `cdn.example.com` that points to your CDN storage location. Pros: - You remain in control of TLS termination and have a better understanding of what content is presented when. If your frontend app uses unique IDs for the static assets, for example `/js/chunk-2d22502a.0844b32d.js`. - Main file **index.html** is served by your server so it can always point to the most up-to-date js/css files. - You can know exactly what's pushed to the CDN. Cons: - You get a new step in your CI/CD pipeline that can fail. If your frontend is deployed but assets failed to sync, your users might get a lot of errors. You also need to ensure that the CDN static files are not simply overwritten (as you might overwrite them while the old frontend app is still using previous files). ### [No CDN](#no-cdn) No CDN, just cache control headers on your web server. This option might work for many cases, however, the first load can be painful if the user is far from your server location and you have a lot of static assets. ### [Pull CDN](#pull-cdn) ![Pull CDN](https://webhookrelay.com/blog/cdn-types-and-setup/images/blog/cdn/cdn.png) CDNs like [BunnyCDN](https://bunnycdn.com/?ref=tftlx880dr) (affiliate link, great service) pull from your origin server but don't try to proxy all your traffic. In this scenario, you serve your **index.html** that then loads assets through the CDN domain instead of your own. Similarly, as with the "Push CDN" type, you will have to either serve assets from `cdn.example.com`, or if you have a fancy global load balancer, you can configure that certain paths load files directly from CDN servers. Pros: - Ease of use. It feels like Cloudflare from the "setup" perspective. You only need to provide it with the address of your web server and then optionally configure your domain. It will pull assets and show nice stats. - Pricing. It seems like it's a lot cheaper than other CDNs while providing an excellent service. They have some comparison info on their pricing page: [https://bunnycdn.com/pricing](https://bunnycdn.com/pricing), however you would need to test it for yourself as it may well depend on your content. Cons: - Need to ensure that your assets have unique build IDs baked into the filenames so you don't serve stale content. Fortunately, most modern javascript transpilers do this by default so in my case with Vue.js I didn't have to do anything on this front. - If CDN would go down, even though your index.html loads, your assets would fail anyway. However, in this case, you would still be able to quickly change the assets domain to your main web server. ## [Setting up BunnyCDN (Pull CDN) in a SPA](#setting-up-bunnycdn-pull-cdn-in-a-spa) I couldn't immediately spot the docs but if you are doing this not for the first time, it's quite straightforward: 1. Create a "pull zone". You will get your pull zone domain which is a reverse proxy to your origin web server: ![Pull zone config](https://webhookrelay.com/blog/cdn-types-and-setup/images/blog/cdn/bunny-cdn.png) 1. (Optional but recommended) Create a CNAME from your domain to the allocated pull zone domain (in our case it's **cdn.webhookrelay.com** -> **webhookrelay.b-cdn.net**). This enables you to load assets from your domain name. 2. Update your webpack config to add asset file prefix. Example for vue.config.js would be: ``` module.exports = { publicPath: process.env.NODE_ENV === 'production' ? 'https://cdn.your-domain-here.com/' : '/', } ``` > If you are using React, check out "Public Folder" docs here: [https://create-react-app.dev/docs/using-the-public-folder/](https://create-react-app.dev/docs/using-the-public-folder/). That's it, generated assets will all have the prefix to load through the CDN. If you are using Nginx to serve your app, ensure that you are providing correct headers for js and css files. For example: ``` location ~* \.(?:css|js)$ { expires 1y; add_header Cache-Control "public"; access_log off; } ``` I hope you will find this useful whenever you decide to add CDN for your website! --- --- title: New feature announcement: domain-based endpoints meta: "og: title": "New feature announcement: domain-based endpoints" description: Introducing new feature: domain based webhook endpoints url: https://webhookrelay.com/blog/domain-based-webhook-endpoints.md file: /blog/domain-based-webhook-endpoints.md --- ![Stripes](https://webhookrelay.com/blog/domain-based-webhook-endpoints/images/stripes.svg) # **New feature announcement: domain-based endpoints** Introducing new feature: domain based webhook endpoints We are happy to announce that new domain-based input endpoints are now available for everyone to use. ![domains, subdomains and paths](https://webhookrelay.com/blog/domain-based-webhook-endpoints/images/docs/webhooks/custom-domain.png) Some people have probably already noticed that you could see one more endpoint in your input settings - `https://my.webhookrelay.com/v1/webhooks/xxxx` (default one) and `https://xxxx.hooks.webhookrelay.com`which is our new one. Building on our new virtual host based router we can also finally allow input endpoints such as `https://hooks.example.com` (you can put in your own domain) and custom paths such as `https://hooks.example.com/github`, `https://hooks.example.com/stripe`, etc.. In this short article we will briefly look into what has changed and what kind of improvements we can expect. ## [Background](#background) When Webhook Relay was initially built, path-based routing solved the issue for systems like Jenkins and pretty much anything else we have encountered. However, there were always some cases such as: - Sensitive payload signing schemes where URL path was also included, therefore sending webhooks to /v1/webhooks/xxxx and then receiving them on `http://internal_server/api/hooks` would result in an invalid signature. - Custom domains for endpoints. - A custom path so you don't have to stay with the same input ID for life. ## [Domain vs path-based endpoints](#domain-vs-path-based-endpoints) First of all, path-based webhook endpoints such as `https://my.webhookrelay.com/v1/webhooks/xxxx` are not being deprecated, they have their use case and they already deliver millions of webhooks per day. However, for each endpoint, you will now be able to also assign custom subdomains and domains. ## [Custom subdomains](#custom-subdomains) Input endpoints can also now utilize custom subdomains. Instead of having an endpoint such as [https://x1scmzopk2ogxxty3qvb4o.hooks.webhookrelay.com](https://x1scmzopk2ogxxty3qvb4o.hooks.webhookrelay.com) you can now specify your own subdomain under `.hooks.webhookrelay.com`, for example [https://dogfood-shop.hooks.webhookrelay.com](https://dogfood-shop.hooks.webhookrelay.com). To get started, either click on "reserve domain" in your input details page or go to [https://my.webhookrelay.com/domains](https://my.webhookrelay.com/domains) and reserve it there. Once you have reserved it, you will be able to select it from the dropdown. ## [Custom domains (hooks.example.com going through Webhook Relay)](#custom-domains-hooksexamplecom-going-through-webhook-relay) Just like with custom subdomains for webhook endpoints you will have to register your own domain either in input details page or [here](https://my.webhookrelay.com/domains). Once it's registered, select it from the dropdown in the input details page. Then, go to your DNS provider and configure a CNAME record pointing at hooks.webhookrelay.com. On the first webhook, Webhook Relay will provision a free certificate for you. ## [Advantages](#advantages) Main advantages of using custom domains for webhook forwarding: - 3rd party software (that sends webhooks) sometimes can't deal with /v1/webhooks/... style endpoints so subdomains become an easy to use solution. - You can delete and re-create your inputs in another bucket. Previously, you had your unique input ID that couldn't be moved. - Even less lock-in. If you are using your own domain, you can always configure it to point at a hole in your firewall instead of Webhook Relay :) - Whitelabel endpoints. With custom domains, your customers will not know that Webhook Relay is accepting requests and processing them, you can put your own domain. - Build your own APIs. With custom domains, paths and [Functions](https://webhookrelay.com/blog/domain-based-webhook-endpoints/docs/webhooks/functions/) you can create a simple API that can respond to some requests directly or pass them to your backend. s You can find more information on domain configuration in the [webhook forwarding documentation](https://docs.webhookrelay.com/quick-start-forwarding-1). --- --- title: Dotscience: Tunnels at Scale for Data Science | WebhookRelay meta: "og: title": "Dotscience: Tunnels at Scale for Data Science" description: A case study on how Dotscience utilizes Webhook Relay tunnels url: https://webhookrelay.com/blog/dotscience-tunnels-jupyter.md file: /blog/dotscience-tunnels-jupyter.md --- ![Stripes](https://webhookrelay.com/blog/dotscience-tunnels-jupyter/images/stripes.svg) # **How Dotscience manages thousands of tunnels to create a better Data Science environment** A case study on how Dotscience utilizes Webhook Relay tunnels ## [The product](#the-product) The core tenet of the Webhook Relay service is receiving and processing HTTP requests. When used with bidirectional tunnels it's a **1:1** relationship that simply exposes the underlying service to the internet. Our client software can run on any machine (Windows, MacOS, Linux x86 and ARM) as well as Docker containers, Kubernetes deployments. When used with webhook forwarding, relationship becomes **N:N**, meaning that there can be multiple public endpoints that will be routing to multiple destinations that are either internal (private network) or public destinations. All HTTP requests can be transformed on arrival to our system or before getting dispatched. A single webhook can be transformed into a new request that is tailored for any API. ## [Webhook Relay masterplan](#webhook-relay-masterplan) - Remove all friction from exposing internal services to the internet for easy access. - Securely let traffic in for sensitive systems (CI/CD) using unidirectional forwarding. - Transform webhooks, when needed. ## [Dotscience](#dotscience) [Dotscience](https://dotscience.com/) is a Machine Learning & Data Science platform that allows engineers and data scientists easily utilize compute infrastructure and track their data in a reproducible way. Since it's an end-to-end Data Science platform, in this article we will only focus on Jupyter notebook service and ML model deployment for inference. ![Dotscience high level](https://webhookrelay.com/blog/dotscience-tunnels-jupyter/images/blog/dotscience/dotscience-high-level.png) ## [Use case: runners anywhere](#use-case-runners-anywhere) The potential problem with Dotscience runners was that they could be started anywhere - in the Kubernetes environment, on a simple virtual machine with docker that is running in a cloud environment, or in a private data center. This required easy access to the Jupyter, free from any restrictions regardless of the environment. The solution was to adopt Webhook Relay tunnels to enable Dotscience users to reach Jupyter notebook servers running on local or remote compute nodes. Dotscience agents would start both Jupyter, [Dotmesh](https://github.com/dotmesh-io/dotmesh) (data persistence daemon), and Webhook Relay container. This container would open a tunnel and serve Jupyter notebooks under a URL that's similar to `xyz.tasks.dotscience.com`. Dotscience dashboard would then open it in an iframe and supply authentication token. Tunnel management is completely automated with domains and routing configuration created during runtime. During normal operation, tunnels might last for a few minutes or even days. There can be thousands of tunnels created and deleted within hours. Since we are utilizing wildcard domains for the tunnels, TLS becomes a simple thing to manage. ## [Use case: ML predictions on any Kubernetes environment](#use-case-ml-predictions-on-any-kubernetes-environment) As with Dotscience runners for Data Science and ML model training on Jupyter notebooks, there is a need for easy access to deployed models on Kubernetes clusters. Since Dotscience provides a deployer model where the user just needs to start the operator, they might not always be able to configure load balancing. For this, Webhook Relay ingress controller comes into play where we can provide access to the models whether it's running in GKE, EKS or Minikube: ![Dotscience ML predictions](https://webhookrelay.com/blog/dotscience-tunnels-jupyter/images/blog/dotscience/tf-serving.png) ## [Key results](#key-results) - Global region for Dotscience cloud offering deployed. - Each self-hosted Dotscience stack includes a standalone Webhook Relay tunneling service for easy access. - Successfully used tunnels to provide connectivity to Jupyter notebooks, front-end connecting to Python cores. - Dotscience operator automatically deploying Webhook Relay ingress controller. - All tunnels created automatically through the API, removed once Jupyter is offline. - ML predictions (inference) are served over the tunnels. --- --- title: Static IPs for webhook calls to enable whitelisting meta: "og: title": "Static IPs for webhook calls to enable whitelisting" description: How to setup static IPs for webhook calls to enable whitelisting url: https://webhookrelay.com/blog/static-ips-for-webhook-whitelisting.md file: /blog/static-ips-for-webhook-whitelisting.md --- ![Stripes](https://webhookrelay.com/blog/static-ips-for-webhook-whitelisting/images/stripes.svg) # **Static IPs for webhook calls to enable whitelisting** How to setup static IPs for webhook calls to enable whitelisting Quite often corporate firewalls only allow incoming webhooks from whitelisted IPs and many public services can't provide them due operational complexity. The complexity for egress traffic usually comes from a modern infrastructure where servers are treated as cattle, not pets. Servers don't have static IPs and only rely on a cloud load balancer that accepts ingress traffic and then routes to servers running in private network. Updates are often done by provisioning a whole new pool of nodes and gradually draining the old one. Generated webhooks can be coming from services that are running on various backends: ![dynamic infrastructure](https://webhookrelay.com/blog/static-ips-for-webhook-whitelisting/images/blog/static-ips/dynamic_infra.png) ## [Solutions for the webhook producer](#solutions-for-the-webhook-producer) To provide a static set of IP address for outgoing connections (so your firewall can whitelist them) a company that runs on cloud infrastructure (such as Kubernetes or serverless infrastructure like AWS Lambda) needs to use cloud provider services like [Cloud NAT on GCP](https://cloud.google.com/nat/docs/overview), [Internet Gateway on AWS](https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Internet_Gateway.html) that route outgoing traffic (which can be a lot) or set up their own dedicated group of instances that will forward all requests: ![Proxy nodes or NAT](https://webhookrelay.com/blog/static-ips-for-webhook-whitelisting/images/blog/static-ips/proxy_or_nat.png) So, we can see that from the webhook producer there are definitely multiple ways to achieve it. However, with the Cloud NAT on GCP it can be quite costly since they would route all traffic through it. And with a separate pool of nodes they would now have to manage a pool of nodes (update OS, update software, update configuration, etc.) for very little added value from their end. This can explain why with time fewer services are providing a set of external static IPs for organizations to whitelist. A more common way (and probably better) to ensure that webhooks are coming from the correct source is with payload signatures. Some good examples are Github: [https://developer.github.com/webhooks/securing/](https://developer.github.com/webhooks/securing/) and Stripe: [https://stripe.com/docs/webhooks/signatures](https://stripe.com/docs/webhooks/signatures). ## [Solution for the webhook receiver (relay agent as a proxy)](#solution-for-the-webhook-receiver-relay-agent-as-a-proxy) One of the solutions that we will demonstrate today will be to use Webhook Relay to accept and forward webhooks from an internal or your controlled static external IP. With internal IP it's simple, you can choose an installation method that you prefer [here](https://docs.webhookrelay.com/installation-options/installation-options) but in this guide we will deploy your controlled GCP server that will act as a relay for all webhooks: ![Using relay agent](https://webhookrelay.com/blog/static-ips-for-webhook-whitelisting/images/blog/static-ips/with_whr_agent.png) Pros of this setup: - No responses are returned from the destination (unless you explicitly turn them on) - Many services can be using the same relay agent as a proxy, just create more buckets with their own inputs and outputs - You get the history of webhook calls for debugging/audit - Relay agent is stateless so you can delete and recreate that VM at any time ### [Prerequisites](#prerequisites) Before we begin, ensure that you have these tools ready: - Terraform - [download from here](https://www.terraform.io/downloads.html) - gcloud - [download from here](https://cloud.google.com/sdk/gcloud/) - git - [download from here](https://git-scm.com/downloads) - Webhook Relay account - [register here](https://my.webhookrelay.com/) ### [Clone repository](#clone-repository) Clone our Terraform repository: ``` git clone https://github.com/webhookrelay/relay-tf.git cd relay-tf ``` ### [Initialize Terraform modules & authentication](#initialize-terraform-modules-authentication) Go into the 'gcp' directory: ``` cd gcp ``` Initialize terraform modules: ``` terraform init ``` Authenticate to your GCP account: ``` gcloud auth application-default login ``` > Instructions how to use service account can be found here: [https://www.terraform.io/docs/providers/google/guides/getting_started.html#adding-credentials](https://www.terraform.io/docs/providers/google/guides/getting_started.html#adding-credentials). ### [Set variables](#set-variables) Let's first create a bucket called **static-ip** here [https://my.webhookrelay.com/buckets](https://my.webhookrelay.com/buckets): ![Created bucket](https://webhookrelay.com/blog/static-ips-for-webhook-whitelisting/images/blog/static-ips/bucket.png) Then, create an output. Make sure 'internal' is chosen to route webhooks through our GCP agent. For the sake of this example we will point the output destination at `http://ifconfig.co/json` to get our IP address: ![Output configuration](https://webhookrelay.com/blog/static-ips-for-webhook-whitelisting/images/blog/static-ips/output.png) Then, go to the access token page here [https://my.webhookrelay.com/tokens](https://my.webhookrelay.com/tokens) and click "Create Token". You will need these details for the terraform inputs file. Create a new `inputs.tfvars` using your favorite text editor, and put the following in it: ``` project = "" relay_key = "" relay_secret = "" relay_buckets = "static-ip" ``` ### [Terraform it!](#terraform-it) Now, to deploy relay agent: ``` terraform apply -var-file inputs.tfvars ``` It will print out ssh command (that you can use to get into the instance) and external IP: ``` Outputs: external_ip_address = 35.185.28.228 gcloud_ssh = gcloud beta compute ssh --zone us-east1-b relay-agent-vm-c34ded9a750faf93 --project webhookrelay ``` ### [Testing it](#testing-it) Now, if you send requests to your bucket's input with a curl (or any other client, in real world scenario it will be sent by the webhook producer's service): ``` curl https://my.webhookrelay.com/v1/webhooks/739903db-ef87-4427-8d78-caccc28253c9 ``` they will be dispatched to the destination through the GCP server and therefore have "35.185.28.228" (different if you have provisioned it yourself) IP. This way you can whitelist webhook source or just ensure that you deploy Webhook Relay agent with terraform into an existing private network. You can view the response from the [http://ifconfig.co/json](http://ifconfig.co/json) service in your bucket details page: ![Response from the destination](https://webhookrelay.com/blog/static-ips-for-webhook-whitelisting/images/blog/static-ips/response.png) Which shows that the IP was in fact our external IP from the agent (35.185.28.228): ``` { "ip": "35.185.28.228", "ip_decimal": 599334116, "country": "United States", "country_eu": false, "country_iso": "US", "hostname": "228.28.185.35.bc.googleusercontent.com", "latitude": 38.6583, "longitude": -77.2481, "asn": "AS15169", "asn_org": "GOOGLE", "user_agent": { "product": "curl", "version": "7.65.3", "raw_value": "curl/7.65.3" } } ``` That's it, if you would like to learn more about Webhook Relay, check out our [docs](https://docs.webhookrelay.com/quick-start-forwarding), [examples](https://docs.webhookrelay.com/short-examples) and this [blog](https://webhookrelay.com/blog/static-ips-for-webhook-whitelisting/blog/) :) --- --- title: Responding to API calls using Node-RED Webhook Relay node meta: "og: title": "Responding to API calls using Node-RED Webhook Relay node" description: How to respond to API calls using Node-RED Webhook Relay node url: https://webhookrelay.com/blog/responding-to-api-calls-on-nodered.md file: /blog/responding-to-api-calls-on-nodered.md --- ![Stripes](https://webhookrelay.com/blog/responding-to-api-calls-on-nodered/images/stripes.svg) # **Responding to API calls using Node-RED Webhook Relay node** How to respond to API calls using Node-RED Webhook Relay node ![API response from Node-RED](https://webhookrelay.com/blog/responding-to-api-calls-on-nodered/images/blog/responding-to-api-calls-on-node-red/cover.png) In the previous article about [controlling gadgets via IFTTT and Node-RED](https://webhookrelay.com/blog/responding-to-api-calls-on-nodered/blog/google-home-ifttt-node-red/) we explored ways to receive webhooks without public IP or configuring NAT and then performing certain actions. However, sometimes you need to respond back to the webhook producers or just other applications that expect success/error responses to properly function. Up until now you would have needed to use [Webhook Relay tunnels](https://docs.webhookrelay.com/products/tunnels) but with the recent release, we allow sending dynamic responses back to the caller. Pros: - No need to expose your Node-RED to the internet. - Respond with carefully crafted HTTP responses choosing your status code, headers and body. This feature transforms Webhook Relay webhook forwarding feature from unidirectional-only to a much more powerful tool. ### [How it works](#how-it-works) Webhook responses work by pausing HTTP response for up to 10 seconds while waiting for the response. The rules are simple: 1. You have to explicitly declare on the Webhook Relay output (via relay CLI or web dashboard) that it should wait for the response. 2. Your application has to send response back to the incoming webhook within 10 seconds. 3. Response cannot be larger than 3MB (usually API responses are a lot smaller). 4. Response has to include original `meta` object that you have received with the webhook (it contains unique request ID and bucket ID that are required by Webhook Relay to correctly respond) ## [Creating an API with Node-RED and Webhook Relay node](#creating-an-api-with-node-red-and-webhook-relay-node) We will create a simple API backend that will return current weather information for a selected city: ![Node-RED responses](https://webhookrelay.com/blog/responding-to-api-calls-on-nodered/images/blog/responding-to-api-calls-on-node-red/node-red-api-response.png) Here we will use several custom nodes nodes: - [node-red-contrib-webhookrelay](https://flows.nodered.org/node/node-red-contrib-webhookrelay) - you can find node installation instructions [in the official docs](https://docs.webhookrelay.com/short-examples/home-automation/node-red). - [node-red-contrib-wait-paths](https://flows.nodered.org/node/node-red-contrib-wait-paths) - [node-red-node-openweathermap](https://flows.nodered.org/node/node-red-node-openweathermap) Other nodes are from the standard palette. > **Note that**: We need to preserve `payload.meta` object from the original Webhook Relay message as we will be using it to reply to the correct request. Your application has 10 seconds to send a reply and there might be several request in-flight that Node-RED is dealing with. ### [Webhook Relay node configuration](#webhook-relay-node-configuration) 1. Go to [buckets page](https://my.webhookrelay.com/buckets) and create a new bucket. 2. Once you have a bucket, open Input settings and select that it should wait for a reply from "Any output". 3. Go to [tokens page](https://my.webhookrelay.com/tokens) and get your key/secret pair. 4. Add bucket name and token key/secret pair to the `node-red-contrib-webhookrelay` node. ### [Configuring the flow](#configuring-the-flow) 1. grab request metadata for later response ![Get paths meta object](https://webhookrelay.com/blog/responding-to-api-calls-on-nodered/images/blog/responding-to-api-calls-on-node-red/get-paths-meta.png) We will have to join this later with the rest of the data to correctly respond to the caller. 1. parse URL query ![Get city name](https://webhookrelay.com/blog/responding-to-api-calls-on-nodered/images/blog/responding-to-api-calls-on-node-red/get-city-name.png) now, since we HTTP requests with a query like ?city=London&country=GB, we need to get these details into an object that openweather node will understand. Here's the code: ``` function getJsonFromUrl(url) { if(!url) url = location.search; var query = url.substr(0); var result = {}; query.split("&").forEach(function(part) { var item = part.split("="); result[item[0]] = decodeURIComponent(item[1]); }); return result; } return { location: getJsonFromUrl(msg.payload.query) } ``` 1. request weather ![Request weather](https://webhookrelay.com/blog/responding-to-api-calls-on-nodered/images/blog/responding-to-api-calls-on-node-red/request-weather.png) 1. encode JSON data - here we just take the response from openweather response and encode it into a JSON string. This payload will be returned to the caller. 2. put message into a "path" for later join - same as number 1: ![Put data into path](https://webhookrelay.com/blog/responding-to-api-calls-on-nodered/images/blog/responding-to-api-calls-on-node-red/data-into-path.png) 1. join metadata and data - time to join weather data and request metadata: ![Join meta and data](https://webhookrelay.com/blog/responding-to-api-calls-on-nodered/images/blog/responding-to-api-calls-on-node-red/join-meta-and-data.png) 1. form API response - use "function" node to grab "meta" and "data" values from the previous node ``` return { meta: msg.paths["meta"].meta, // this is original meta field from the payload (it's important to include it so we have the message ID) status: 200, // status code to return (200, 201, 400, etc) body: msg.paths["data"].data, // body headers: { 'content-type': ['application/json'] // good practice to include content type, browsers do their best to display it nicely } }; ``` That's it, connect the response node back to the Webhook Relay node and open your Bucket's input URL in your browser or just use `curl` (if you are on Linux or Mac): ``` curl https://my.webhookrelay.com/v1/webhooks/YOUR-INPUT-UUID?city=London&country=GB ``` ### [What's next](#whats-next) Try out integrating different APIs. If free tier is too low for you, message me and I will bump up your limits :) Here's the flow itself, feel free to import and play with it. ``` [{"id":"44a1295a.6a99d","type":"tab","label":"Node-RED API","disabled":false,"info":""},{"id":"5b239f76.3f86d8","type":"webhookrelay","z":"44a1295a.6a99d","buckets":"node-red-responses","x":160,"y":320,"wires":[["329e9d6b.728d6a","8858f09a.c7aee8"]]},{"id":"ee1d7dc4.e7c358","type":"function","z":"44a1295a.6a99d","name":"create response","func":"return {\n meta: msg.paths[\"meta\"].meta, // this is original meta field from the payload (it's important to include it so we have the message ID)\n status: 200, // status code to return (200, 201, 400, etc)\n\tbody: msg.paths[\"data\"].data, // body\n\theaders: {\n\t 'content-type': ['application/json']\n\t}\n};","outputs":1,"noerr":0,"x":1280,"y":320,"wires":[["5b239f76.3f86d8"]]},{"id":"c3ea1e3b.a8c708","type":"openweathermap","z":"44a1295a.6a99d","name":"","wtype":"current","lon":"","lat":"","city":"","country":"","language":"en","x":510,"y":460,"wires":[["27524c01.87056c"]]},{"id":"329e9d6b.728d6a","type":"function","z":"44a1295a.6a99d","name":"get city name","func":"function getJsonFromUrl(url) {\n if(!url) url = location.search;\n var query = url.substr(0);\n var result = {};\n query.split(\"&\").forEach(function(part) {\n var item = part.split(\"=\");\n result[item[0]] = decodeURIComponent(item[1]);\n });\n return result;\n}\n\nreturn {\n location: getJsonFromUrl(msg.payload.query)\n}","outputs":1,"noerr":0,"x":250,"y":460,"wires":[["c3ea1e3b.a8c708"]]},{"id":"27524c01.87056c","type":"json","z":"44a1295a.6a99d","name":"encode","property":"payload","action":"","pretty":false,"x":720,"y":460,"wires":[["13242f86.520e8"]]},{"id":"cf7bfe30.6a52b8","type":"wait-paths","z":"44a1295a.6a99d","name":"wait for meta and data","paths":"[\"data\",\"meta\"]","timeout":15000,"finalTimeout":60000,"x":1020,"y":320,"wires":[["ee1d7dc4.e7c358"]]},{"id":"13242f86.520e8","type":"change","z":"44a1295a.6a99d","name":"paths[\"data\"]","rules":[{"t":"move","p":"payload","pt":"msg","to":"paths[\"data\"].data","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":930,"y":460,"wires":[["cf7bfe30.6a52b8"]]},{"id":"8858f09a.c7aee8","type":"change","z":"44a1295a.6a99d","name":"paths[\"meta\"]","rules":[{"t":"move","p":"payload.meta","pt":"msg","to":"paths[\"meta\"].meta","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":570,"y":320,"wires":[["cf7bfe30.6a52b8"]]},{"id":"78312bed.0624fc","type":"comment","z":"44a1295a.6a99d","name":"1. grab request metadata for later response","info":"","x":660,"y":260,"wires":[]},{"id":"d13cd56d.3e6a08","type":"comment","z":"44a1295a.6a99d","name":"2. parse URL query","info":"","x":270,"y":400,"wires":[]},{"id":"b0668370.e60a88","type":"comment","z":"44a1295a.6a99d","name":"3. request weather","info":"","x":510,"y":400,"wires":[]},{"id":"a9a0d2e1.527298","type":"comment","z":"44a1295a.6a99d","name":"4. encode json","info":"","x":740,"y":400,"wires":[]},{"id":"fe57aa5.43ee4d8","type":"comment","z":"44a1295a.6a99d","name":"5. put message into a \"path\" for later join","info":"","x":1020,"y":400,"wires":[]},{"id":"d0d96c0d.930398","type":"comment","z":"44a1295a.6a99d","name":"6. join metadata and data","info":"","x":1030,"y":260,"wires":[]},{"id":"f25f94a5.7ad5d8","type":"comment","z":"44a1295a.6a99d","name":"7. form API response","info":"","x":1300,"y":260,"wires":[]}] ``` --- --- title: Docker Compose update on Github webhook | WebhookRelay meta: "og: title": "Docker Compose update on Github webhook" description: Learn how to update Docker Compose on push to Github using webhooks url: https://webhookrelay.com/blog/docker-compose-update-on-github-webhooks.md file: /blog/docker-compose-update-on-github-webhooks.md --- ![Stripes](https://webhookrelay.com/blog/docker-compose-update-on-github-webhooks/images/stripes.svg) # **Docker Compose update on Github webhook** Learn how to update Docker Compose on push to Github using webhooks ![webhook filter signature](https://webhookrelay.com/blog/docker-compose-update-on-github-webhooks/images/blog/docker-compose-update-on-github-webhooks/docker-update-on-webhook.png) Last year I wrote a blog post about [combining several tools](https://webhookrelay.com/blog/docker-compose-update-on-github-webhooks/blog/2018/07/17/auto-deploy-on-git-push/) to automate simple NodeJS app updates on git push. Many users were solving similar problems by writing local web servers in Ruby, Python or PHP to receive webhooks and to do the update. I am happy to announce that we have decided to add this feature to the [relay](https://docs.webhookrelay.com/installation-options/installation-options/install-cli) client. Now, to execute a bash script, you can: ``` relay forward --bucket my-bucket-name --relayer exec --command bash update.sh ``` And to launch a Python app on webhook: ``` relay forward --bucket my-bucket-name --relayer exec --command python my-app.py ``` This opens up some interesting possibilities to create pipelines that can react to pretty much anything that emits webhooks. In this article I will show you how to build a GitOps style pipeline that does [Docker Compose](https://docs.docker.com/compose/) update to sync with a docker-compose.yaml hosted on a git repository. **Prerequisites** - Docker & [Docker Compose](https://docs.docker.com/compose/install/) - [Webhook Relay account](https://my.webhookrelay.com/login) with configured [relay CLI](https://docs.webhookrelay.com/installation-options/installation-options/install-cli) - [Github account](https://github.com) Repository with scripts that I used for this article can be found here: [https://github.com/webhookrelay/docker-compose-update-on-git-push](https://github.com/webhookrelay/docker-compose-update-on-git-push). ## [Step 1: Deploying containers through Docker Compose](#step-1-deploying-containers-through-docker-compose) First step is to do the initial deployment. We will create a simple dockerized Python application that you can find [here](https://github.com/webhookrelay/docker-compose-update-on-git-push) that connects to Redis and deploy it: ``` version: '3' services: web: image: "karolisr/python-counter:0.1.0" ports: - "5000:5000" redis: image: "redis:alpine" ``` ## [Step 2: Setting up updates on Github tag](#step-2-setting-up-updates-on-github-tag) Since we only want to update on git tags and not just any pushes, let's configure a webhook and analyze the payload. To achieve that, let's first create a bucket with an internal output: ``` $ relay forward --bucket docker-compose-update-on-git-push http://localhost:4000 Forwarding: https://my.webhookrelay.com/v1/webhooks/a956a9f7-2260-4bc2-a54b-3d896acf4206 -> http://localhost:4000 Starting webhook relay agent... 2019-08-28 23:14:41.773 INFO using standard transport... 2019-08-28 23:14:41.928 INFO webhook relay ready... {"host": "my.webhookrelay.com:8080", "buckets": ["8e977e70-09a6-464c-ad30-855e1cd5d9f9"]} ``` Here, bucket will be used later to subscribe to github requests while destination is just a mandatory argument that we don't have to use in this case. Grab that [https://my.webhookrelay.com/v1/webhooks/](https://my.webhookrelay.com/v1/webhooks/)*** URL and go to your repository's settings -> webhooks section. Once there, set: - Payload URL to your unique [https://my.webhookrelay.com/v1/webhooks/](https://my.webhookrelay.com/v1/webhooks/)*** URL - Content type to _application/json_ - Secret to a random secret name, for the sake of this example my secret will be 'webhooksecret' - Click `Let me select individual events.` and select _Releases_. Now, go to your repository's releases page (for example [https://github.com/webhookrelay/docker-compose-update-on-git-push/releases](https://github.com/webhookrelay/docker-compose-update-on-git-push/releases)) and make a new release `1.0.0`. Then, if you visit bucket details page or [logs page](https://my.webhookrelay.com/logs) - you should see webhook from Github. Open it and let's inspect the payload. It's quite lengthy but we should be able to see ``` "action": "released", ``` in the top. To ensure that we only react on these events, create a rule: ![webhook filter](https://webhookrelay.com/blog/docker-compose-update-on-github-webhooks/images/blog/docker-compose-update-on-github-webhooks/filter.png) If you tag another release, you should see now that only one webhook was forwarder ![rule stopped webhook](https://webhookrelay.com/blog/docker-compose-update-on-github-webhooks/images/blog/docker-compose-update-on-github-webhooks/rule_action.png) ## [Step 3: Update script and starting relay background service](#step-3-update-script-and-starting-relay-background-service) Our update script is: ``` #!/bin/bash git pull docker-compose up -d ``` It will pull the latest compose file and update containers. Now, let's update configuration in `relay.yml` file ([access key & secret can be generated here](https://my.webhookrelay.com/tokens)): ``` version: v1 key: xxx # your access key secret: xxx # your access secret buckets: - docker-compose-update-on-git-push # your bucket name where github webhooks are sent relayer: type: exec command: bash commandArgs: - /full/path/to/docker-compose-update-on-git-push/update.sh # <-- should be full path to your update script timeout: 300 ``` To start the relay, run: ``` relay run -c relay.yml ``` This will be running it through your terminal. For production use cases, please use [background service mode](https://docs.webhookrelay.com/installation-options/installation-options/background-service). It will ensure that the daemon is launched on OS startup. ## [Let's try it out](#lets-try-it-out) Launch docker-compose: ``` docker-compose up -d ``` Check containers: ``` $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 26cd2219e18b redis:alpine "docker-entrypoint.s…" 20 seconds ago Up 3 seconds 6379/tcp docker-compose-update-on-git-push_redis_1 63c8cd1ae7bb karolisr/python-counter:0.1.0 "flask run" 20 seconds ago Up 18 seconds 0.0.0.0:5000->5000/tcp docker-compose-update-on-git-push_web_1 $ curl http://localhost:5000 I have been seen 1 times. $ curl http://localhost:5000 I have been seen 2 times. ``` Next step would be to build a new image `0.2.0` and push it to the registry. Once it's available, we can update our github repository `docker-compose.yml` and make a new release. For the sake of this example, let's do this through the Github UI. In a few seconds you should see a new container running: ``` $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 27b2542423ec karolisr/python-counter:0.2.0 "flask run" 9 seconds ago Up 7 seconds 0.0.0.0:5000->5000/tcp docker-compose-update-on-git-push_web_1 26cd2219e18b redis:alpine "docker-entrypoint.s…" 10 minutes ago Up 9 minutes 6379/tcp docker-compose-update-on-git-push_redis_1 ``` ## [Optional: Validating Github secret](#optional-validating-github-secret) Webhook Relay output rules can also validate Github signature: ![webhook filter signature](https://webhookrelay.com/blog/docker-compose-update-on-github-webhooks/images/blog/docker-compose-update-on-github-webhooks/signature.png) This will ensure that only webhooks signed by Github will be processed. ## [Conclusion](#conclusion) As with any code executed on your machine - you have to be careful when automating tasks. Webhook Relay will provide you with a unidirectional flow of webhooks into the machine. Your script/applications are on your machine and cannot be modified remotely through Webhook Relay. Coupled with authenticated webhook endpoints (you can configure it on a bucket level) or webhook payload checksum validation - you can build a secure update mechanism. In general this is an easy way and simple way to update Docker Compose on your server without investing time. You can just push Docker images, update the tag and relay agent will run the update. --- --- title: Automated Jenkins builds on GitHub pull request meta: "og: title": "Automated Jenkins builds on GitHub pull request" description: Configuration example on how to automatically start builds on GitHub pull request url: https://webhookrelay.com/blog/automated-github-pull-request-builds-on-jenkins.md file: /blog/automated-github-pull-request-builds-on-jenkins.md --- ![Stripes](https://webhookrelay.com/blog/automated-github-pull-request-builds-on-jenkins/images/stripes.svg) # **Automated Jenkins builds on GitHub pull request** Configuration example on how to automatically start builds on GitHub pull request ![workflow](https://webhookrelay.com/blog/automated-github-pull-request-builds-on-jenkins/images/blog/jenkins-pull-request/cover.png) In this short guide we will configure Jenkins to start builds on GitHub pull requests. Subsequent builds will be triggered on any new commits and GitHub pull request status will show whether build succeeded or failed. This setup will work without configuring router, firewall or having a public IP. It will also work behind a corporate firewall. ## [Getting a VM](#getting-a-vm) In my case, I just grabbed a Vagrant box from [https://app.vagrantup.com/ubuntu/boxes/xenial64](https://app.vagrantup.com/ubuntu/boxes/xenial64): ``` Vagrant.configure("2") do |config| config.vm.box = "ubuntu/xenial64" config.vm.network "private_network", type: "dhcp" end ``` Then: ``` vagrant up vagrant ssh ``` And we have our VM. You can get the IP address by typing `ifconfig` in the terminal. ## [Installing Jenkins](#installing-jenkins) I mostly followed this guide [https://linuxize.com/post/how-to-install-jenkins-on-ubuntu-18-04/](https://linuxize.com/post/how-to-install-jenkins-on-ubuntu-18-04/). The only caveat I encountered this time with Jenkins, was the [jdk version mismatch](https://stackoverflow.com/questions/39621263/jenkins-fails-when-running-service-start-jenkins). ## [Connecting to Jenkins](#connecting-to-jenkins) First, get your Jenkins token: ``` cat /var/lib/jenkins/secrets/initialAdminPassword ce04d19270934633a7badcac3cfac316 ``` Then, either open your node firewall (or check Vagrant port forwarding) **or** do the easy thing: connect with the relay: #### [Get CLI](#get-cli) To get the CLI, check [instructions here](https://docs.webhookrelay.com/installation-options/installation-options/install-cli). On a 64-bit Linux OS it's: ``` curl -sSL https://storage.googleapis.com/webhookrelay/downloads/relay-linux-amd64 > relay \ && chmod +wx relay && sudo mv relay /usr/local/bin ``` #### [Login](#login) Go to [https://my.webhookrelay.com/tokens](https://my.webhookrelay.com/tokens), click **CREATE TOKEN** and copy/paste login command into the terminal, it should be something like: ``` relay login -k -s ``` #### [Start tunnel](#start-tunnel) ``` $ relay connect :8080 Connecting: http://lsw7eq49jlhsuldvhpiyku.webrelay.io <----> http://127.0.0.1:8080 https://lsw7eq49jlhsuldvhpiyku.webrelay.io <----> http://127.0.0.1:8080 ``` Now, open the browser. You should see a similar screen: ![Jenkins login view](https://webhookrelay.com/blog/automated-github-pull-request-builds-on-jenkins/images/blog/jenkins-pull-request/jenkins-login.png) Follow the steps to configure your Jenkins initial admin user. ## [Installing the plugin](#installing-the-plugin) Plugin installation instructions [can be found here](https://wiki.jenkins.io/display/JENKINS/GitHub+pull+request+builder+plugin). Once you have it, add GitHub credentials - your username and GitHub token. ## [Forwarding configuration](#forwarding-configuration) Go to your [bucket configuration](https://my.webhookrelay.com/buckets) and create a bucket called `github-webhooks`. Configure it to forward all webhooks to [http://localhost:8080/](http://localhost:8080/). This will ensure that webhooks will reach Jenkins server. ![forwarding config](https://webhookrelay.com/blog/automated-github-pull-request-builds-on-jenkins/images/blog/jenkins-pull-request/whr-bucket-configuration.png) Once you have the `relay` CLI on the machine where you run Jenkins, type: ``` relay forward --bucket github-webhooks ``` This will start forwarding webhooks. There are alternative options to run the forwarding daemon, such as [Docker container](https://docs.webhookrelay.com/installation-options/containerized/docker). > If you are creating webhook configuration manually in GitHub, use [http://localhost:8080/ghprbhook](http://localhost:8080/ghprbhook) destination as it's the endpoint on which the plugin is listening for webhooks. In default case, Jenkins will automatically transform [https://my.webhookrelay.com/v1/webhooks/21e13033-bd3d-47a2-bf15-6fd42d4b40a3](https://my.webhookrelay.com/v1/webhooks/21e13033-bd3d-47a2-bf15-6fd42d4b40a3) endpoints to [https://my.webhookrelay.com/v1/webhooks/21e13033-bd3d-47a2-bf15-6fd42d4b40a3/ghprbhook](https://my.webhookrelay.com/v1/webhooks/21e13033-bd3d-47a2-bf15-6fd42d4b40a3/ghprbhook) and Webhook Relay will preserve the extra `/ghprbhook` path. Now, configure **GitHub Pull Request Builder**: ![pr builder config](https://webhookrelay.com/blog/automated-github-pull-request-builds-on-jenkins/images/blog/jenkins-pull-request/jenkins-pr-builder-config.png) ## [Creating a job](#creating-a-job) To create a new job, first select "Freestyle project", then: Add the project's GitHub URL to the "GitHub project" field (the one you can enter into browser. eg: " [https://github.com/rusenask/jenkins-test/](https://github.com/rusenask/jenkins-test/)"): ![jenkins set github project](https://webhookrelay.com/blog/automated-github-pull-request-builds-on-jenkins/images/blog/jenkins-pull-request/jenkins-job-1.png) Configure **Source Code Management** section: - Select Git SCM. - Add your GitHub "Repository URL". - Under Advanced, set "refspec" to `+refs/pull/*:refs/remotes/origin/pr/*` - In "Branch Specifier", enter `${ghprbActualCommit}` ![jenkins set source code management](https://webhookrelay.com/blog/automated-github-pull-request-builds-on-jenkins/images/blog/jenkins-pull-request/jenkins-job-2-scm.png) Configure **Build Triggers** with a list of admins and tick the `use github hooks for build triggering`: ![jenkins build triggers](https://webhookrelay.com/blog/automated-github-pull-request-builds-on-jenkins/images/blog/jenkins-pull-request/jenkins-job-3-build-triggers.png) Add your **Build** step configuration. This can be anything you want, usually people tend to use either a bash script, Makefile target or something specific to your programming language such as `go build`: ![jenkins build triggers](https://webhookrelay.com/blog/automated-github-pull-request-builds-on-jenkins/images/blog/jenkins-pull-request/jenkins-job-4-build.png) ## [Opening a PR](#opening-a-pr) Now, whenever you open a new pull request in GitHub, you should see a build being triggered: ![pr triggering build](https://webhookrelay.com/blog/automated-github-pull-request-builds-on-jenkins/images/blog/jenkins-pull-request/pr-open.png) You can view build status in your Jenkins instance as well. This build indicator in GitHub will either turn red or green based on the build status. ## [Conclusion](#conclusion) As we can see, there are several required steps to make sure your PRs get automatically built and tested when using Jenkins. Those can be split into two groups: 1. System configuration that involves: - setting Webhook Relay agent to forwarding webhooks - installing and configuring [GitHub pull request builder plugin](https://wiki.jenkins.io/display/JENKINS/GitHub+pull+request+builder+plugin) 1. Whenever you create a new project in Jenkins, setting up few settings. The first time I did this it took me some time to go through the configuration options, but second and third time didn't take more than 1 minute :) I hope you will find this guide useful! --- **P.S. Bonus troubleshooting below:** When we are talking about Jenkins, there are many ways for things to go wrong. Multiple plugin versions, corporate proxies and different operating systems contribute to all of this. I have compiled a short list of items for you to check if you encounter problems. ### [Ensuring webhook configuration](#ensuring-webhook-configuration) Make sure there's an automatically created GitHub repository webhook configuration: ![GitHub repo webhook configuration](https://webhookrelay.com/blog/automated-github-pull-request-builds-on-jenkins/images/blog/jenkins-pull-request/github-webhook-config.png) ### [Ensuring Webhook Relay agent can connect](#ensuring-webhook-relay-agent-can-connect) Normally, connected agent should look like this: ``` relay forward --bucket github-webhooks Filtering on bucket: github-webhooks Starting webhook relay agent... 1.55523552627511e+09 info using standard transport... 1.5552355264042027e+09 info webhook relay ready... {"host": "my.webhookrelay.com:8080"} ``` If you are behind a corporate proxy, try adding `--ws` flag to change default transport type from GRPC to WebSocket: ``` relay forward --ws --bucket github-webhooks Filtering on bucket: github-webhooks Starting webhook relay agent... 1.5552387754607065e+09 info using websocket based transport... 1.5552387754607568e+09 info authenticating to 'wss://my.webhookrelay.com:443/v1/socket'... 1.5552387754608495e+09 info websocket reader process started... 1.555238775470567e+09 info subscribing to buckets: [github-webhooks ``` ### [Check logs](#check-logs) There will be several sources of logs you can check out: - Jenkins logs under `/log/all`: ![jenkins logs](https://webhookrelay.com/blog/automated-github-pull-request-builds-on-jenkins/images/blog/jenkins-pull-request/jenkins-logs.png) - Webhook Relay forwarding logs: ![forwarding logs](https://webhookrelay.com/blog/automated-github-pull-request-builds-on-jenkins/images/blog/jenkins-pull-request/forwarding-logs.png) - CLI logs: ``` relay forward --bucket github-webhooks Filtering on bucket: github-webhooks Starting webhook relay agent... 1.55523552627511e+09 info using standard transport... 1.5552355264042027e+09 info webhook relay ready... {"host": "my.webhookrelay.com:8080"} 1.555236773074343e+09 info webhook request relayed {"destination": "http://localhost:8080/ghprbhook/", "method": "POST", "bucket": "github-webhooks", "status": 200, "retries": 0} 1.5552368184301443e+09 info webhook request relayed {"destination": "http://localhost:8080/ghprbhook/", "method": "POST", "bucket": "github-webhooks", "status": 200, "retries": 0} 1.5552368215106862e+09 info webhook request relayed {"destination": "http://localhost:8080/ghprbhook/", "method": "POST", "bucket": "github-webhooks", "status": 200, "retries": 0} 1.555236829308788e+09 info webhook request relayed {"destination": "http://localhost:8080/ghprbhook/", "method": "POST", "bucket": "github-webhooks", "status": 200, "retries": 0} 1.5552368314174337e+09 info webhook request relayed {"destination": "http://localhost:8080/ghprbhook/", "method": "POST", "bucket": "github-webhooks", "status": 200, "retries": 0} 1.555236920064973e+09 info webhook request relayed {"destination": "http://localhost:8080/ghprbhook/", "method": "POST", "bucket": "github-webhooks", "status": 200, "retries": 0} 1.5552369202506151e+09 info webhook request relayed {"destination": "http://localhost:8080/ghprbhook/", "method": "POST", "bucket": "github-webhooks", "status": 200, "retries": 0} ``` --- --- title: Rules-based webhook filtering & routing | WebhookRelay meta: "og: title": "Rules-based webhook filtering & routing" description: Example use-case of rules-based routing and filtering for GitHub webhooks url: https://webhookrelay.com/blog/webhook-rule-based-filters.md file: /blog/webhook-rule-based-filters.md --- ![Stripes](https://webhookrelay.com/blog/webhook-rule-based-filters/images/stripes.svg) # **Rules-based webhook filtering & routing** Example use-case of rules-based routing and filtering for GitHub webhooks ![cover](https://webhookrelay.com/blog/webhook-rule-based-filters/images/blog/output-rules/cover.png) Some webhook providers do not offer fine-grained control over what events are being sent via webhooks. Sometimes, you end up in a situation where webhooks, based on their type have to be delivered to different servers. We will have a look at a new Webhook Relay forwarding feature that allows users to define rules on outputs that help in these situations. Each output can now have one or more, multi-level rules and rule groups. In this short tutorial, we will create a rule to verify GitHub webhooks and route them to appropriate endpoints based on their contents. ## [Preparing webhooks bucket](#preparing-webhooks-bucket) First, go to the Webhook Relay [buckets page](https://my.webhookrelay.com/buckets) and create a new bucket. Once you have it, you should be able to set your public input endpoint: ![bucket input](https://webhookrelay.com/blog/webhook-rule-based-filters/images/blog/output-rules/bucket-input.png) Buckets are a grouping mechanism that let you define multiple inputs and multiple output destinations. ## [Creating a new GitHub webhook](#creating-a-new-github-webhook) To create a GitHub repository webhooks, go to **Settings** -> **Webhooks** and set few parameters: - **Payload URL** should be your input endpoints: `https://my.wehbookrelay.com/v1/webhooks/.....`. - **Content type** is whatever your webhook receiver is comfortable consuming, usually it will be `application/json. - **Secret** is your secret key, set it to anything you want, I set mine to `very-secret` :) ![setting up GitHub webhooks](https://webhookrelay.com/blog/webhook-rule-based-filters/images/blog/output-rules/github-webhook.png) ## [X-Hub-Signature rule](#x-hub-signature-rule) Open your bucket details page from the [buckets page](https://my.webhookrelay.com/buckets) and create a new output (mine relays to a helper service on [https://bin.webhookrelay.com](https://bin.webhookrelay.com)): ![creating bucket output](https://webhookrelay.com/blog/webhook-rule-based-filters/images/blog/output-rules/bucket-output.png) Once you have your output, click on a triangle to define your rules. For now, we will create a single rule. Click on the dropdown next to "ADD RULE", select **header** and then click the "ADD RULE" button. In the parameter set **X-Hub-Signature**. It tells Webhook Relay where to find the parameter to match against. Then, from the dropdown select **payload SHA1** and set the last field to your secret value, mine was `very-secret`: ![github webhook signature matcher](https://webhookrelay.com/blog/webhook-rule-based-filters/images/blog/output-rules/signature-matcher.png) Configuration is now set. Try pushing to your repository. Webhook should be pass through the service. Feel free to change your secret either in Webhook Relay configuration or GitHub webhook config and retrying push, it will be rejected. At this point, we can determine whether webhooks are coming from GitHub or not. Next step - differentiating between pushes and tags. ## [Detecting tags](#detecting-tags) If you create a new release or just make a git tag on your repository, a new webhook will be visible in your repository. Go to request details: ![github tag webhook details](https://webhookrelay.com/blog/webhook-rule-based-filters/images/blog/output-rules/request-details.png) The part that's interesting for us is `"ref": "refs/tags/0.1.0"`: ``` { "ref": "refs/tags/0.1.0", "before": "0000000000000000000000000000000000000000", "after": "39237a56aab6ed53030b086e6d02d04aa232b337", "created": true, "deleted": false, "forced": false, ... ... ``` And the second webhook will be for the tag itself: ``` { "ref": "0.1.1", "ref_type": "tag", "master_branch": "master", "description": null, "pusher_type": "user", ... ... ``` Let's create a rule that will catch any tag requests: 1. Create a new output called **production-ci**. 2. Copy existing signature matcher rule from the first output (click on a JSON VIEW when in the rules modal and copy it into the new output rule). 3. Add a new group and set **Logical Operator** to **Any**. This works the same way as **or** in programming. 4. Add a new rule that will be matching **payload**, set parameter to **ref** (it will be getting it from payload), select **contains** matcher and to the value field set **refs/tags**: ![tags matcher rule](https://webhookrelay.com/blog/webhook-rule-based-filters/images/blog/output-rules/tags-rule.png) Now, if you push again, webhook will not be relayed to **production-ci** destination. Let's modify our first rule to reject all tag webhooks. You can do that by creating a similar rule to the one we added in **production-ci** output but instead of **contains** set it to **does not contain**. Now, make a new tag and check out your bucket logs: ![bucket logs](https://webhookrelay.com/blog/webhook-rule-based-filters/images/blog/output-rules/logs-table.png) ## [Next steps](#next-steps) As you see, it's trivial to set up multi-level rules that can route webhooks to desired destinations based on their contents and also do some basic validation. If you need yet more power on selecting correct webhooks, check out our example implementation of the [socket protocol](https://docs.webhookrelay.com/products/webhooks/websocket-server) here: [https://github.com/webhookrelay/webhookrelay-ws-client](https://github.com/webhookrelay/webhookrelay-ws-client). Using WebSockets, you can receive webhooks directly inside your application and process them accordingly. Library is already used in Node-RED workflows via our node ([blog post here](https://webhookrelay.com/blog/webhook-rule-based-filters/blog/2019/01/09/nodered-owntracks-direct/)). I have recently also had a chance to look at Rust programming language and found that implementing a webhook forwarding application can be quite interesting: [https://github.com/rusenask/rust-ws-client](https://github.com/rusenask/rust-ws-client). --- --- title: Using Google Firestore for a Golang backend application meta: "og: title": "Using Google Firestore for a Golang backend application" description: Switching from internal KV store to a Google Firestore can be quick and easy url: https://webhookrelay.com/blog/using-google-firestore-for-go-backend.md file: /blog/using-google-firestore-for-go-backend.md --- ![Stripes](https://webhookrelay.com/blog/using-google-firestore-for-go-backend/images/stripes.svg) # **Using Google Firestore for a Golang backend application** Switching from internal KV store to a Google Firestore can be quick and easy ![cover](https://webhookrelay.com/blog/using-google-firestore-for-go-backend/images/blog/using-gcp-firestore-for-go-backend/gcp-firestore-golang.png) Usually, when I need a database I just pick [Postgres](https://www.postgresql.org/) or embedded key-value stores such as the excellent [boltdb](https://github.com/etcd-io/bbolt), [badger from dgraph](https://github.com/dgraph-io/badger) or [Redis](https://redis.io/) (if I need a KV store but shared between several nodes). With flexibility comes the burden of maintenance and sometimes additional cost. In this article, we will explore a simple Golang backend service that will use Google Firestore as storage. When I started working on a simple project called [bin.webhookrelay.com](https://bin.webhookrelay.com/), I picked Badger as a key-value store, attached a persistent disk to a Kubernetes pod and launched it. > [bin.webhookrelay.com](https://bin.webhookrelay.com) is a free service that allows you to capture webhook or API requests for testing purposes. It also lets you specify what response body and status code to return, as well as set an optional response delay. The data model was (and still is) simple: - **Bin** that has a certain configuration like what status code to return, response body, content-type header, and response delay. - **Request** which is the actual captures webhook request with bin ID, request body, headers, and query. Most of the time, KV stores such as boltdb or badger are great for such use cases. Problems arise when you want to either scale horizontally or have a rolling update strategy meaning that a number of instances of your application would have to surge during the update. While Kubernetes is great for running pretty much any workload, an update where it has to detach a persistent disk and reattach it to a new pod can lead to downtime and just generally slow updates. I always try to avoid such scenarios, however, webhook bin service was suffering from it. This time, I decided to try out [Cloud Firestore](https://firebase.google.com/docs/firestore/). You probably have already heard about Firestore (previously known as Firebase) and that it is very popular amongst mobile app developers who need to have a database for their Android and iOS apps. Apparently, it can also provide a really nice developer UX for backend applications! :) ## [Setting it up](#setting-it-up) For authentication, Golang Firestore client uses a standard mechanism that relies on a service account. Basically, you need to go to: 1. [GCP console](https://console.cloud.google.com) 2. Click on IAM & admin 3. Go to service accounts (on the left navigation bar) 4. Create a new service account with Firestore permissions 5. Download the file, you can now use it for authentication > Docs can be found [in the Google Cloud authentication section](https://cloud.google.com/docs/authentication/getting-started). Application code is surprisingly simple. You get the client using Google application credentials, project ID and that's it: ``` func NewFirestoreBinManager(opts *FirestoreBinManagerOpts) (*FirestoreBinManager, error) { ctx := context.Background() var options []option.ClientOption if opts.CredsFile != "" { options = append(options, option.WithCredentialsFile(opts.CredsFile)) } // credentials file option is optional, by default it will use GOOGLE_APPLICATION_CREDENTIALS // environment variable, this is a default method to connect to Google services client, err := firestore.NewClient(ctx, opts.ProjectID, options...) if err != nil { return nil, err } return &FirestoreBinManager{ binsCollection: opts.BinsCollection, // our bins collection name reqsCollection: opts.ReqsCollection, // our requests collection name client: client, pubsub: opts.Pubsub, logger: opts.Logger, }, nil } ``` ## [Adding and updating bins](#adding-and-updating-bins) When creating a document, you can specify document ID and just pass in the whole golang struct without first marshaling it into JSON: ``` func (m *FirestoreBinManager) BinPut(ctx context.Context, b *bin.Bin) (err error) { _, err = m.client.Collection(m.binsCollection).Doc(b.GetId()).Set(ctx, b) return err } ``` Note that we supply collection name: `Collection(m.binsCollection)`, ID: `Doc(b.GetId())` and set the struct fields `Set(ctx, b)`. This really saves time! An alternative with a KV store would be something like: ``` func (m *BinManager) BinPut(ctx context.Context, b *bin.Bin) (err error) { b.Requests = nil encoded, err := proto.Marshal(b) if err != nil { return err } return m.storage.Store(ctx, "bins/"+b.Id, encoded, nil) } ... // store package func (s *Storage) Store(ctx context.Context, id string, data []byte, metadata map[string]string) error { err := s.db.Update(func(txn *badger.Txn) error { // Your code here… return txn.Set([]byte(id), data) }) return err } ``` ## [Deleting](#deleting) To delete a bin in our case means deleting both the bin document and all associated webhook requests with it: ``` func (m *FirestoreBinManager) BinDelete(ctx context.Context, binID string) error { _, err := m.client.Collection(m.binsCollection).Doc(binID).Delete(ctx) if err != nil { m.logger.Errorw("failed to delete bin doc by ref", "error", err, ) } // Now, get all the requests and delete them in a batch request iter := m.client.Collection(m.reqsCollection).Where("Bin", "==", binID).Documents(ctx) numDeleted := 0 batch := m.client.Batch() for { doc, err := iter.Next() if err == iterator.Done { break } if err != nil { return fmt.Errorf("Failed to iterate: %v", err) } batch.Delete(doc.Ref) numDeleted++ } // If there are no documents to delete, // the process is over. if numDeleted == 0 { return nil } _, err = batch.Commit(ctx) return err } ``` ## [Limitations](#limitations) While it is very easy to store, retrieve and modify documents, some people will miss SQL type queries that can aggregate, count records and do other useful operations in the database. For example, to track document counts, you will have to implement a solution [similar to one described here](https://cloud.google.com/firestore/docs/solutions/counters). My suggestion would be to spend more time planning data structure and what kind of operations are you planning to use before embarking on this journey :) ## [Conclusion](#conclusion) While being a bit skeptical at first, I quickly started liking Firestore. While running a managed Postgres would allow me to easier switch cloud providers, it would also make it more expensive to run. Keeping storage interface small means that you can implement Postgres (or any other database) driver in a matter of hours so then the most important things to look for are: - How much maintenance the solution requires - Cost - Performance Useful resources: - [Golang Firestore quickstart](https://github.com/GoogleCloudPlatform/golang-samples/tree/master/firestore/firestore_quickstart) - [Golang Firestore snippets](https://github.com/GoogleCloudPlatform/golang-samples/tree/master/firestore/firestore_snippets) - [GCP Authentication](https://cloud.google.com/docs/authentication/getting-started) - [Firestore Billing](https://firebase.google.com/docs/firestore/pricing) --- --- title: Cloudflare Support for Home Assistant | WebhookRelay meta: "og: title": "Cloudflare Support for Home Assistant" description: Webhook Relay Home Assistant remote access add-on now support Cloudflare Domains url: https://webhookrelay.com/blog/cloudflare-support-for-home-assistant.md file: /blog/cloudflare-support-for-home-assistant.md --- ![Stripes](https://webhookrelay.com/blog/cloudflare-support-for-home-assistant/images/stripes.svg) # **Introducing Cloudflare support for Home Assistant remote access** Webhook Relay Home Assistant remote access add-on now support Cloudflare Domains ![Home Assistant Cloudflare](https://webhookrelay.com/blog/cloudflare-support-for-home-assistant/images/ha-cloudflare.png) Today we are expanding Webhook Relay's Home Assistant add-on support for portability across different domains by announcing integration with [Cloudflare](https://www.cloudflare.com/) API to create and manage DNS records. This means that you can transfer your domain management to Cloudflare and start enjoying new capabilities. Cloudflare has also recently announced their [registrar](https://www.cloudflare.com/products/registrar/) so instead of just managing your records (they are doing it for free) and providing great proxy that speeds up your websites, you can now fully transfer your domain to them and avoid increasing prices from your original registrar. Webhook Relay Home Assistant add-on is a lightweight service that creates fast and secure tunnels for remote connection. It empowers users and expands their choice when ISPs or routers prevent incoming connections. With this add-on users can easily use Alexa, Google Home, IFTTT and many other automation services that require your Home Assistant be reachable from the internet. Add-on itself is a single executable, just 7MB size and barely uses any CPU & RAM, making it an ideal option when running on low power devices. With Cloudflare integration, other than allowing you to have any domain names with TLS pass-through tunnels, you get some additional benefits: - Minification - Remove unwanted characters like whitespaces, comments, new line characters and block delimiters which are not needed for a web page to serve. - Cloud WAF - WAF (Web Application Firewall) helps to keep your site secure from OWASP top 10, CMS (WordPress, Joomla, etc. ) vulnerabilities. Cloudflare WAF has got more than 145 rules to protect you from almost all types of web applications attack. - Browser Caching - Optimized Network Routing ## [Getting Started](#getting-started) Add-on is installed in a same way as before (you can follow [official documentation](https://docs.webhookrelay.com/short-examples/home-automation/home-assistant) on it). The only difference now is that you will need to get a Cloudflare API key. Follow these [instructions](https://support.cloudflare.com/hc/en-us/articles/200167836-Where-do-I-find-my-CloudFlare-API-key-), or: - Login to the Cloudflare account. - Go to My Profile. - Scroll down to API Keys and locate Global API Key. - Click API Key to see your API identifier. > Note that your Cloudflare API will always remain on the device and will never be shared with Webhook Relay cloud service. Now, set: - Webhook Relay key and secret from [tokens page](https://my.webhookrelay.com/tokens) - Cloudflare email and API key - your tunnel domain ``` { "key": "[YOUR TOKEN KEY]", "secret": "[YOUR TOKEN SECRET]", "forwarding": [ ], "tunnels": [{ "name": "home-assistant", "destination": "http://127.0.0.1:8123/", "protocol": "tls", "domain": "home-assistant.example.com", "provider": "cloudflare" }], "duck_dns": { "token": "", "accept_terms": false }, "cloudflare": { "email": "your-email@example.com", "api_key": "[YOUR CLOUDFLARE API KEY]" }, "tunnels_enabled": true, "forwarding_enabled": false } ``` Don't forget to set the new `"provider": "cloudflare"` field in the tunnel configuration. ## [Commitment to portability and privacy](#commitment-to-portability-and-privacy) We released multi-architecture add-on with the initial 1.0.0 release that can work in any environment, as long as it can connect to the public Webhook Relay servers. These agents are now running from low-power Raspberry Pi devices to high-performance servers, providing secure tunnels. With TLS pass-through tunnels we created an easy to use, secure by default tunnels where the agent is doing the heavy-lifting of ensuring TLS certificates and decrypting traffic. In this configuration, Webhook Relay servers can only see encrypted traffic ensuring maximum privacy. _Webhook Relay is a modern tunnelling service available on multiple architectures and providing both free and paid tiers. Self-hosted & enterprise are available. Follow us on Twitter [@webhookrelay](https://twitter.com/webhookrelay)._ --- --- title: Node-RED OwnTracks location tracking without public IP/MQTT meta: "og: title": "Node-RED OwnTracks location tracking without public IP/MQTT" description: How to get webhooks from OwnTracks to Node-RED without public IP or configuring NAT url: https://webhookrelay.com/blog/nodered-owntracks-direct.md file: /blog/nodered-owntracks-direct.md --- ![Stripes](https://webhookrelay.com/blog/nodered-owntracks-direct/images/stripes.svg) # **Node-RED OwnTracks location tracking without public IP/MQTT** How to get webhooks from OwnTracks to Node-RED without public IP or configuring NAT ![cover of node-red, owntracks and webhook relay](https://webhookrelay.com/blog/nodered-owntracks-direct/images/blog/nodered-owntracks/cover.png) If you want to track your location but don't want it to be shared with way too many companies that shouldn't know it, have a look at this fantastic, Open Source mobile application [OwnTracks](https://owntracks.org/) ([https://github.com/owntracks](https://github.com/owntracks)) that can send phone's location data to your chosen server. No external service that aggregates your data is required, allowing you to do pretty much anything you want with the gathered location data. OwnTracks can send payloads either to an MQTT server or a standard HTTP endpoint. It seems like a lot of people are running MQTT services on cloud providers such as Bluemix and then forward those messages to their private networks that are running at home or some other, firewalled location. In this short tutorial, we will use [Node-RED](https://nodered.org) to receive, process and visualize our location data. Location data will be encrypted during transport with TLS and also using libsodium ([secret-key authenticated encryption](https://download.libsodium.org/doc/secret-key_cryptography)), right till the end before it reaches the map: ![high-level diagram of node-red and owntracks](https://webhookrelay.com/blog/nodered-owntracks-direct/images/blog/nodered-owntracks/node-red-owntracks.png) No router, firewall configuration or public IP will be required, as Webhook Relay will be providing a public endpoints to receive webhooks and then send them over to our Node-RED instance over a WebSocket. > If your Node-RED is accessible from the internet, you can skip Webhook Relay part and just add an HTTP handler to handle OwnTracks webhooks. Check [Node-RED security docs](https://nodered.org/docs/security) and ensure that your instance has authentication enabled. ## [Prerequisites](#prerequisites) - [Webhook Relay account](https://my.webhookrelay.com) - instead of MQTT, we will be using HTTP webhooks so we don't have to run a separate service. Also, Webhook Relay enables you to receive webhooks without having a public IP or configuring NAT. - [Node-RED instance](https://nodered.org) - I assume you have it running, if not, [installation instructions can be found here](https://nodered.org/docs/getting-started/installation). - [node-red-contrib-webhookrelay](https://flows.nodered.org/node/node-red-contrib-webhookrelay) node so we can subscribe to webhooks. - [node-red-contrib-owntracks](https://flows.nodered.org/node/node-red-contrib-owntracks) node to decrypt location data. - [node-red-contrib-web-worldmap](https://flows.nodered.org/node/node-red-contrib-web-worldmap) node to display a map and put a location marker on it. You can either install nodes via `npm` or click 'Manage Palette' inside the Node-RED settings. ## [Create a public endpoint for webhooks & install the app](#create-a-public-endpoint-for-webhooks-install-the-app) Our location data will be relayed to Node-RED through a public Webhook Relay address. Therefore, the first thing to do will be setting up your endpoint: 1. Go to [buckets page](https://my.webhookrelay.com/buckets) and click _CREATE BUCKET_. Name it 'owntracks'. Buckets are a grouping mechanism to aggregate webhooks and fan-out them to multiple destinations. As we will be using WebSockets, we don't need to create outputs (you can create one if you want a resend functionality to work from the web interface). 2. Once you create the bucket, note the public endpoint URL which is in the format of `https://my.webhookrelay.com/v1/webhooks/[REPLACE ME WITH YOUR OWN ENDPOINT]`. You will need to supply this to your OwnTracks app. Install & configure the app: 1. Go to [https://owntracks.org/](https://owntracks.org/) and follow instructions based on your phone's operating system. 2. Once you have the app, go to the settings and set the mode to HTTP and the endpoint should be set to the Webhook Relay input URL that you got previously. Don't set `Secret encryption key` option yet, if you want to have a look at the data structure. Once you have set these settings, you can manually publish your location from the app. Webhook logs should start appearing in your bucket details page or in the [logs](https://my.webhookrelay.com/logs). ## [Data structure](#data-structure) If you don't set 'Secret encryption key' inside the OwnTracks application, you can view webhooks on your Webhook Relay [logs page](https://my.webhookrelay.com/logs). An example payload: ``` { "batt": 69, "lon": 0.008261475503538551, "acc": 2000, "p": 102.94917297363281, "vac": 44, "topic": "owntracks/kr-1/EEIR6BC7-17AB-57AD-803E-651363E02256", "lat": 52.416367098924324, "conn": "w", "tst": 1546786399, "alt": 10, "_type": "location", "tid": "kr" } ``` Since it's a JSON payload, it makes working with it a lot easier. According to [Socket Server documentation](https://docs.webhookrelay.com/products/webhooks/websocket-server), our actual payload will be inside `payload.body` variable when it comes out from the WebSocket. While decrypted data is all nice and good, we can encrypt it to get an additional layer of security on top of already existing TLS. Go ahead to the application and set `Secret encryption key` to your secret. Now the payload data should look like: ``` { "_type": "encrypted", "data": "edxJuWXnAOWvWdIHx1wfu6/IQsRQf5Gs3XFqRY4gQU1p3NLV2Upr8u4u8Lu4p9x+nEPnONKy0Gio1qumdjJV6n+U6YUqkj9mZIUVuyHznNuLM2zzaGKuTFFQeJjo+AjRYtnlK4rSsQou6LnaDcT9lqKYZinWESNWsg6qIKcfx8jsl2f//dSDzH02VBvO0Dg5iqupf9ZWBMuQbO9/EPvBtkaaOt0c41dfQUR3+4YY8cQx+FXB9yWHPyuyKlxAU+vAgSo6QAyJE4h4z9ZuD4y5SYkZ35Rp+QS8tsI0CNTUzA551Jo4EsWl7dwcTfbYyQB+7sDU3yFhD3oLAuwPOCRdvHLlpGS0G3D6T/ujU8pKkJj5ldT8Sw==" } ``` ## [Creating the flow](#creating-the-flow) ![geo flow](https://webhookrelay.com/blog/nodered-owntracks-direct/images/blog/nodered-owntracks/geo-flow.png) The actual flow is straightforward once we know what kind of data we are receiving and what kind of data the world map expects. Let's see what kind of configuration [is required according to node-red-contrib-web-worldmap](https://flows.nodered.org/node/node-red-contrib-web-worldmap): ``` msg.payload = { name:"Jason", lat:51.05, lon:-1.35 } ``` We can also include `icon` field to distinguish multiple trackers. ### [Step 1: getting the data](#step-1-getting-the-data) We will be using [node-red-contrib-webhookrelay](https://flows.nodered.org/node/node-red-contrib-webhookrelay) node to receive webhooks from Webhook Relay bucket that is receiving OwnTracks webhooks. Go to the [tokens page](https://my.webhookrelay.com/tokens), generate a key & secret token pair and add them to the node. Set bucket to `owntracks` (it has to match the bucket name in Webhook Relay that is receiving the webhooks). Once it's there, try deploying the flow and see whether the node goes into **connected** state. ### [Step 2: extracting the payload](#step-2-extracting-the-payload) Webhook Relay node messages contain bucket metadata, headers, request query, method, and body. However, in this case, we only care about the body so let's extract it. Create a function node with the following code: ``` return { payload: msg.payload.body }; ``` ### [Step 3: decrypt OwnTracks location data](#step-3-decrypt-owntracks-location-data) Once we have the payload, we need to decrypt it. Add [node-red-contrib-owntracks](https://flows.nodered.org/node/node-red-contrib-owntracks) node and connect an output from step 2 into it. Open node settings and set the same secret as you set in your phone's app (`Secret encryption key`). No additional configuration is required here, it will return the original, decrypted payload. ### [Step 4: format JSON](#step-4-format-json) In this step, we can customize decrypted location data before passing it into the world map node. The following example just adds an icon, but feel free to modify it however you want according to the docs: ``` return { payload: { name: msg.payload.tid, lat: msg.payload.lat, lon: msg.payload.lon, icon: 'user-circle-o' } }; ``` ### [Step 5: sending location data to the map](#step-5-sending-location-data-to-the-map) This step is quite simple. Connect the output from step 4 to both `worldmap` and `tracks` nodes. This will always update the pointer on the map while sending data also to the `tracks` node which draws lines on the map as the object moves. Configure the `worldmap` node with an increased Max Age and also configure the tracked points on the `tracks` node to whatever number you like. ### [State management](#state-management) By default, world map component will only display real-time information of newly created location tracking points. You can use **status** node to get the status from the world map when new sessions are connected and just replay the data. You can store points in [postgres](https://flows.nodered.org/node/node-red-contrib-postgres) or [sqlite](https://flows.nodered.org/node/node-red-node-sqlite), depending on your needs. If you only care about geofence actions, state is probably not required. ## [Bonus: geofence triggers](#bonus-geofence-triggers) You can connect also ingest data from the OwnTracks decryption node with [node-red-node-geofence](https://flows.nodered.org/node/node-red-node-geofence) node to create geofence based triggers: ![geo flow](https://webhookrelay.com/blog/nodered-owntracks-direct/images/blog/nodered-owntracks/geofence.png) Use this to create automated actions that get triggered when trackers leave or enter areas (for example your car leaving the town while your phone is at home at 3 am). ## [Bonus: Home Assistant](#bonus-home-assistant) If you are using Home Assistant and find yourself behind double NAT from your ISP or just can't have a public IP, have a look at our [add-on](https://webhookrelay.com/blog/nodered-owntracks-direct/blog/2018/10/12/hassio-tls-tunnels-duckdns/) to create secure, TLS pass-through tunnels for remote access. Detail documentation can be found [here](https://docs.webhookrelay.com/short-examples/home-automation/home-assistant). ### [References](#references) - An excellent article about decrypting OwnTracks location data: [https://www.hardill.me.uk/wordpress/2016/04/26/owntracks-encrypted-location-node-red-node/](https://www.hardill.me.uk/wordpress/2016/04/26/owntracks-encrypted-location-node-red-node/) - [Webhook Relay Node-RED guide](https://discourse.nodered.org/t/announce-node-red-contrib-webhookrelay/5989) --- --- title: Remote YouTube downloader Slack bot | WebhookRelay meta: "og: title": "Remote YouTube downloader Slack bot" description: A short tutorial to help you build a remote YouTube video downloader using WebSockets and Slack url: https://webhookrelay.com/blog/remote-tube-downloader.md file: /blog/remote-tube-downloader.md --- ![Stripes](https://webhookrelay.com/blog/remote-tube-downloader/images/stripes.svg) # **Remote YouTube downloader Slack bot** A short tutorial to help you build a remote YouTube video downloader using WebSockets and Slack ![Slash command workflow](https://webhookrelay.com/blog/remote-tube-downloader/images/blog/remote-tube-downloader/slash.png) In this short tutorial, we will see how easy it is to receive webhooks directly inside your applications using Webhook Relay [Socket Server](https://docs.webhookrelay.com/products/webhooks/websocket-server). Our application will be a [Node.js](https://nodejs.org/en/) daemon process which listens for the webhooks and downloads videos that the user (you) requests. While the daemon can be written in any language, I chose JavaScript for this one since it's very easy to read and most of the people have encountered it in the wild. For the webhook producer, we will use Slack and their slash commands. Advantages of [Slack slash commands](https://api.slack.com/slash-commands): - No need for Slack client SDK - Works through webhooks that can easily be processed in any language - No authentication token required (just needs a webhook endpoint where to send requests) ### [Problem](#problem) Webhooks are awesome but your application needs to be exposed to the internet to receive them. It must also have a web server that can process those webhooks. For a simple application or that doesn't have to be always online, configuring NAT/firewall might be an overkill. ### [Solution](#solution) Webhook Relay + WebSocket client can let your applications receive webhooks from 3rd party services without having a public IP/domain, configuring NAT, and even the web server becomes unnecessary. You can process webhooks right inside your application. ## [Prerequisites](#prerequisites) 1. Webhook Relay [account](https://my.webhookrelay.com/register) 2. Slack account and workspace 3. [npm](https://www.npmjs.com) and Node.JS on your machine ## [Create a webhook forwarding bucket](#create-a-webhook-forwarding-bucket) We will be to create a bucket (buckets are used to group inputs/outputs in Webhook Relay) to capture and relay Slack webhooks. Go to [buckets page](https://my.webhookrelay.com/buckets) and create a new bucket called `slash`. It will get a default public endpoint which we will be using in the next step. Create an internal output too, the destination doesn't matter but it will help us with debugging: ![Slash Bucket](https://webhookrelay.com/blog/remote-tube-downloader/images/blog/remote-tube-downloader/slack-bucket.png) ## [Creating Slack slash command app](#creating-slack-slash-command-app) First things first, we will need an easy way to send a webhook. I initially thought about trying out Airtable and Google Sheets but was quite disappointed with the lack of webhooks in their services. Zapier seems to be trying to help a bit there, but their webhooks can only work every 15 minutes and even then I didn't receive any webhooks.. :) So, another obvious and easy choice would be [Slack slash commands](https://api.slack.com/slash-commands). > Slash Commands let users trigger an interaction with your app directly from the message box in Slack. Creating a Slash command actually is a lot more straightforward than you would expect, have a look [in the official docs](https://api.slack.com/slash-commands#creating_commands). Come up with a name and select a workspace: ![Slack App create](https://webhookrelay.com/blog/remote-tube-downloader/images/blog/remote-tube-downloader/slack-app-create.png) Now, from 'Add features and functionality' select **Slash Commands** and fill in some details: ![Slack slash command configure](https://webhookrelay.com/blog/remote-tube-downloader/images/blog/remote-tube-downloader/slash-command-create.png) You can get creative with the command prefix `/dl` as our application doesn't consume it, only the text after the command will be used inside the code. In the Request URL add your Webhook Relay input endpoint that is `https://my.webhookrelay.com/v1/webhooks/....`. Once you are happy with the details, save and install it: ![Slack slash command configure](https://webhookrelay.com/blog/remote-tube-downloader/images/blog/remote-tube-downloader/slack-install-app.png) Now, whenever we type `/dl https://www.youtube.com/watch?v=tPEE9ZwTmy0` it will send a webhook to Webhook Relay input and from there we can relay it to our app. Try it out, it will be captured. ## [Analysing Slack command payload](#analysing-slack-command-payload) Once we have got the test payload, we need to check what's inside it. Unfortunatelly I couldn't find a way to make it send JSON and so we will have to work with `Content-Type: [ "application/x-www-form-urlencoded" ]`. Webhook Relay helpfully parses the message: ``` token: gp9AyhCMqffRI2pahREQrg2S team_id: T3QT2DM0Q team_domain: webhookrelay channel_id: C3RLQ5C4C channel_name: general user_id: U3S9BEU6B user_name: karolis command: /dl text: https://www.youtube.com/watch?v=tPEE9ZwTmy0 response_url: https://hooks.slack.com/commands/T3QT2DM0Q/495886160947/J0YHv46ot6nZNeHiGR4a1I12 trigger_id: 495384852801.126920463024.5438c17b6d97870e3a05f145c6d4bc70 ``` So we now know what fields are we going to take: - **text** - this is our URL to download - **response_url** - this is where we can reply to the user ## [Creating our application](#creating-our-application) Application code can be found on [Github repo](https://github.com/webhookrelay/slack-slash-downloader), feel free to clone it. To make our life easier, some main libraries: - [https://github.com/przemyslawpluta/node-youtube-dl](https://github.com/przemyslawpluta/node-youtube-dl) - download the videos. - [https://github.com/ljharb/qs](https://github.com/ljharb/qs) - query string parses (helps us parse Slack payload). - [https://github.com/request/request](https://github.com/request/request) - simple HTTP client. Even though I would always prefer using a stdlib, in this case it just worked better for me. For readers, here's the whole application: ``` // app.js const WebSocket = require('ws'); var fs = require('fs'); var youtubedl = require('youtube-dl'); var crypto = require('crypto'); var qs = require('qs'); var request = require('request'); var server = 'wss://my.webhookrelay.com/v1/socket'; var reconnectInterval = 1000 * 3; var ws; var apiKey = process.env.RELAY_KEY; var apiSecret = process.env.RELAY_SECRET; var connect = function () { ws = new WebSocket(server); ws.on('open', function () { console.log('Connected, sending authentication request'); ws.send(JSON.stringify({ action: 'auth', key: apiKey, secret: apiSecret })); }); ws.on('message', function incoming(data) { var msg = JSON.parse(data); if (msg.type === 'status' && msg.status === 'authenticated') { console.log('Authenticated, subscribing to the bucket...') ws.send(JSON.stringify({ action: 'subscribe', buckets: ['slash'] })); return } if (msg.type === 'webhook') { processWebhook(qs.parse(msg.body)) } }); ws.on('error', function () { console.log('socket error'); }); ws.on('close', function () { console.log('socket closed, reconnecting'); setTimeout(connect, reconnectInterval); }); }; var processWebhook = function (payload) { console.log('URL: ', payload.text) var tempFilename = 'tmp-' + crypto.randomBytes(4).readUInt32LE(0); var actualFilename = '' var video = youtubedl(payload.text, ['--format=18'], { cwd: __dirname }); // Will be called when the download starts. video.on('info', function (info) { console.log('Download started'); console.log('Filename: ' + info._filename); // saving filename for the later rename actualFilename = info._filename; console.log('Size: ' + info.size); }); video.pipe(fs.createWriteStream(tempFilename)); video.on('end', function () { console.log('Finished downloading!'); // renaming file to the actual video name from our temp one if (actualFilename !== '') { fs.rename(tempFilename, actualFilename, function (err) { if (err) console.log('ERROR: ' + err); }); } // sending response back to Slack respond(payload, { response_type: 'in_channel', text: \`${actualFilename} finished downloading!\` }) }); } var respond = function (payload, result) { request.post( payload.response_url, { json: result }, function (error, response, body) { if (!error && response.statusCode == 200) { console.log(body) return } console.log(error) } ); } connect(); ``` Install dependencies from [https://github.com/webhookrelay/slack-slash-downloader/blob/master/package.json](https://github.com/webhookrelay/slack-slash-downloader/blob/master/package.json) by: ``` npm install ``` ## [Running the app](#running-the-app) To launch it, retrieve the tokens from [tokens page](https://my.webhookrelay.com/tokens) and set them as an environment variables: ``` export RELAY_KEY=your-token-key export RELAY_SECRET=your-token-secret ``` and then: ``` node app.js ``` Once it's running, you can start downloading videos via Slack, just type: ``` /dl https://www.youtube.com/watch?v=H_4eRD8aegk ``` ![Slack slash command result](https://webhookrelay.com/blog/remote-tube-downloader/images/blog/remote-tube-downloader/slash-command-result.png) App logs should be similar to this: ``` URL: https://www.youtube.com/watch?v=H_4eRD8aegk Download started Filename: justforfunc #43 - Migrating Go Modules to v2+-H_4eRD8aegk.mp4 Size: 61705357 Finished downloading! ok ``` Your file should appear in the same directory as the application: ``` ➜ slack-slash-downloader git:(master) ✗ ls -alh total 97M drwxrwxr-x 4 karolis karolis 4.0K Dec 5 23:37 . drwxr-xr-x 12 karolis karolis 4.0K Dec 4 10:40 .. -rw-rw-r-- 1 karolis karolis 2.8K Dec 4 23:11 app.js drwxrwxr-x 8 karolis karolis 4.0K Dec 4 10:40 .git -rw-rw-r-- 1 karolis karolis 27 Dec 4 23:11 .gitignore -rw-rw-r-- 1 karolis karolis 59M Dec 5 23:37 'justforfunc #43 - Migrating Go Modules to v2+-H_4eRD8aegk.mp4' drwxrwxr-x 61 karolis karolis 4.0K Dec 4 12:07 node_modules -rw-rw-r-- 1 karolis karolis 38M Dec 4 11:29 'Overwatch Winter Wonderland Details Leaks, Skins and More!--b-DktHyWJk.mp4' -rw-rw-r-- 1 karolis karolis 386 Dec 4 12:07 package.json -rw-rw-r-- 1 karolis karolis 16K Dec 4 12:07 package-lock.json ``` You can modify the code to put it into your downloads directory or wherever you want to keep the files. ## [To Sum Up](#to-sum-up) In this technical demo we showed a practical use case of how a lightweight Node.js daemon can receive and process webhooks without having public IP or running a web server. This can greatly simplify the whole application, reduce attack surface and provide audit capabilities on top of it. --- --- title: Introducing WebSocket Server | WebhookRelay meta: "og: title": "Introducing WebSocket Server" description: Listen for new webhooks directly from your application using websockets url: https://webhookrelay.com/blog/introducing-websocket-server.md file: /blog/introducing-websocket-server.md --- ![Stripes](https://webhookrelay.com/blog/introducing-websocket-server/images/stripes.svg) # **Introducing WebSocket Server** Listen for new webhooks directly from your application using websockets ![Socket Server picture](https://webhookrelay.com/blog/introducing-websocket-server/images/blog/introducing-socket-server/socket-server.jpg) For the past few months, our users have pointed out that they need an easier way to receive webhooks inside their applications. In short, they do not want to implement a web server just for one endpoint that does nothing but receives webhooks. We have evaluated several solutions on streaming events to clients but ended up using WebSockets. This is because WebSockets is a standard that is implemented in pretty much every programming language (another solution, which we use internally and was considered first, was [NATS](https://www.nats.io/) but it would have required too many modifications). ## [How do WebSockets fit into Webhook Relay?](#how-do-websockets-fit-into-webhook-relay) We originally started with [webhookrelayd](https://hub.docker.com/r/webhookrelay/webhookrelayd/) Docker image, then added [relay](https://webhookrelay.com/blog/introducing-websocket-server/docs/installation/cli/) CLI and Kubernetes [ingress controller](https://docs.webhookrelay.com/installation-options/containerized/kubernetes-installation#option-4-ingress-controller). While these tools did fulfill the requirements most of the time, in some cases a deeper integration was desired. WebSocket clients can subscribe to multiple buckets (which are like _topics_ or _subjects_, if you are familiar with messaging systems) and then it's up to the user to decide what to do with the payload. Clients get Webhook Relay configuration, request headers, and body. Socket servers are designed to be efficient with long-running connections that are created by daemon processes. ## [JavaScript Example](#javascript-example) 1. Log into your account and go to [buckets page](https://my.webhookrelay.com/buckets) 2. Create a bucket called `my-bucket` 3. Ensure that on the outputs side 'WebSocket streaming' is enabled Now, let's create our NodeJS application. The code is straightforward, WebSocket library is doing most of the heavy lifting here: ``` // client.js const WebSocket = require('ws'); var server = 'wss://my.webhookrelay.com/v1/socket'; var reconnectInterval = 1000 * 3; var ws; var apiKey = process.env.RELAY_KEY; var apiSecret = process.env.RELAY_SECRET; var connect = function(){ ws = new WebSocket(server); ws.on('open', function() { console.log('connected, sending authentication request'); ws.send(JSON.stringify({ action: 'auth', key: apiKey, secret: apiSecret })); }); ws.on('message', function incoming(data) { console.log(data) // parse message and if we have authenticated, subscribe to our bucket var msg = JSON.parse(data); if (msg.type === 'status' && msg.status === 'authenticated') { ws.send(JSON.stringify({ action: 'subscribe', buckets: ['my-bucket'] })); } }); ws.on('error', function() { console.log('socket error'); }); ws.on('close', function() { console.log('socket closed, reconnecting'); setTimeout(connect, reconnectInterval); }); }; connect(); ``` Here, we see that: - Once the connection is established, our agent sends an **auth** request. - Once authenticated, the agent sends a **subscribe** event. - If disconnected, just reconnect. > Clients can subscribe to multiple buckets. If a bucket list is empty (`{'action': 'subscribe'}`) then the client is subscribed to **all** buckets) To run this application, let's first install the websocket `ws` library: ``` npm i ws ``` Set token key and secret (from [tokens page](https://my.webhookrelay.com/tokens)): ``` export RELAY_KEY=your-token-key export RELAY_SECRET=your-token-secret ``` Start it: ``` node client.js ``` Once the authentication response is received, we send a `subscribe` event to start streaming. Send some webhooks to your bucket's public input endpoint. Client in action: ``` $ node client.js connected, sending authentication request {"type":"status","status":"authenticated","message":"connected successfully, subscribe to buckets"} {"type":"status","status":"subscribed","message":"subscribed to buckets: my-bucket"} {"type":"webhook","meta":{"bucked_id":"89e44c32-27ff-4832-8655-8a42d3851b6f","bucket_name":"my-bucket","input_id":"ee4ac550-12a4-41a7-837d-dd3356ed1771","input_name":"Default public endpoint","output_name":"22","output_destination":"https://bin.webhookrelay.com/v1/webhooks/e9274477-c8a7-4497-8f7a-a10d7fcb6ba9"},"headers":{"User-Agent":["insomnia/6.2.3"],"Cookie":["__cfduid=dc244a014f0b1e2965544ddb483c3fe1b1525866866"],"Content-Type":["application/json"],"Accept":["*/*"],"Content-Length":["15"]},"query":"foo=bar","body":"{\"hi\": \"there\"}","method":"POST"} {"type":"webhook","meta":{"bucked_id":"89e44c32-27ff-4832-8655-8a42d3851b6f","bucket_name":"my-bucket","input_id":"ee4ac550-12a4-41a7-837d-dd3356ed1771","input_name":"Default public endpoint","output_name":"111","output_destination":"https://bin.webhookrelay.com/v1/webhooks/a0572aa4-bb90-4638-b97c-a37654798c73"},"headers":{"Content-Type":["application/json"],"Accept":["*/*"],"Content-Length":["15"],"User-Agent":["insomnia/6.2.3"],"Cookie":["__cfduid=dc244a014f0b1e2965544ddb483c3fe1b1525866866"]},"query":"foo=bar","body":"{\"hi\": \"there\"}","method":"POST"} ``` Socket Server documentation can be found [here](https://docs.webhookrelay.com/webhooks/websocket-server). ## [So, what can you do with Webhook Relay WebSockets?](#so-what-can-you-do-with-webhook-relay-websockets) Our mission is to connect servers and services that are otherwise hard to reach. Webhook Relay provides the greatest value when you are operating in internal networks, your own computer or your servers can change IP addresses (or there are too many of them to give them unique addresses in the first place). Therefore, consider using WebSockets for: - **Any** service that needs to receive webhook notifications but doesn't have a web server or if web server cannot be used for this purpose. - IoT devices - by subscribing multiple devices to the same webhooks bucket, all requests will be streamed to all subscriptions. - [Node-RED](https://nodered.org/) - start a workflow via a WebSocket event without exposing your Node-RED server to the internet. - Integrating [IFTTT](https://ifttt.com/) and [Zapier](https://zapier.com/) directly into your application. Just set up a webhook action on those systems to send a request to Webhook Relay and your application will be able to process it internally. - Process Stripe webhooks directly inside your application without running a web server. - Easily receive webhooks from [Slack](https://slack.com/) [slash commands](https://api.slack.com/slash-commands) and process them. - Custom processing of webhooks that are relayed to another service. Use Webhook Relay WebSockets to subscribe to an active bucket, for example webhooks from SendGrid could be relayed to multiple marketing systems and/or internal one as well. ## [To Sum Up](#to-sum-up) A new streaming API: - Uses WebSockets as a transport mechanism to enable direct integration into your applications - Messages are encoded in JSON format - Clients are expected to send 2 types of messages: **auth** & **subscribe** Sounds interesting? Do you need help integrating Webhook Relay into your system? [Send us an email](https://webhookrelay.com/blog/introducing-websocket-server//mailto:info@webhookrelay.com) --- --- title: Rancher - push to deploy workflow with Keel | WebhookRelay meta: "og: title": "Rancher - push to deploy workflow with Keel" description: Configuring push to deploy workflow with Rancher and Keel url: https://webhookrelay.com/blog/rancher-push-to-deploy-workflow.md file: /blog/rancher-push-to-deploy-workflow.md --- ![Stripes](https://webhookrelay.com/blog/rancher-push-to-deploy-workflow/images/stripes.svg) # **Rancher - push to deploy workflow with Keel** Configuring push to deploy workflow with Rancher and Keel ![Rancher Webhook Relay and Keel](https://webhookrelay.com/blog/rancher-push-to-deploy-workflow/images/blog/rancher-push-to-deploy/rancher-whr-keel.png) In this article, we will show how easy it is to configure "push to deploy" workflows with Rancher and Keel. If you are not familiar with Rancher, please visit [their excellent website](https://rancher.com/). This article might look lengthy but it's only because there are a lot of screenshots to help you guide through Rancher's UI. Also, once the initial setup of DockerHub, Webhook Relay and Keel is done, all the cluster workloads will get a self-service style automated updates. ### [Short intro to Rancher](#short-intro-to-rancher) It's a fantastic tool that lets you provision and manage Kubernetes clusters. To see how it is different from other tools, check out [this page](https://rancher.com/what-is-rancher/how-is-rancher-different/). It's similar to OpenShift but seems a lot more lightweight. Unlike Rancher, OpenShift only supports a single Kubernetes cluster per OpenShift deployment and does not support running on cloud-hosted Kubernetes services such as GKE, EKS and AKS. ### [Short intro to Keel](#short-intro-to-keel) [Keel](https://github.com/keel-hq/keel) is a continuous delivery tool aimed at Kubernetes that features: - **No CLI/API** - **Self-service** functionality (you just add labels or annotations to your deployments and that's it, no need to configure Keel directly) - **Stateless** - no database/cache - **Policy driven updates** ([SemVer](https://semver.org/), regexp, etc.) - each deployment can have different update policies - **Integrates with major Docker registries** through webhooks and polling, configures subscriptions to Google Cloud GCR Keel is used by hundreds of companies in production every day and saved countless hours for me! ## [Prerequisites](#prerequisites) - Kubernetes cluster (in my case I am using [Minikube](https://github.com/kubernetes/minikube/releases)) - Webhook Relay account and [relay CLI](https://webhookrelay.com/blog/rancher-push-to-deploy-workflow/docs/installation/cli/) - [Rancher](https://rancher.com) ([here's an article on how to set it up with Minikube or Docker for Mac](https://thepracticalsysadmin.com/test-rancher-2-0-using-minikube/)) Good thing about this setup when pairing Keel with Webhook Relay is that it will work anywhere as long as there's an internet connectivity. Webhooks will be delivered to private networks as well. ## [Configuring Keel](#configuring-keel) ### [Create a new namespace 'keel'](#create-a-new-namespace-keel) Go to your current project or create a new project and create a new namespace called `keel`. ### [Add your key & secret to the cluster](#add-your-key-secret-to-the-cluster) Since we are going to use Webhook Relay sidecar to receive webhooks inside your cluster, it will need authentication details. Go to [https://my.webhookrelay.com/tokens](https://my.webhookrelay.com/tokens) and click **CREATE TOKEN**. Grab your key and secret. Now, go to your project > resources > secrets: ![Rancher secrets](https://webhookrelay.com/blog/rancher-push-to-deploy-workflow/images/blog/rancher-push-to-deploy/rancher-secrets.png) And create a new secret with key/value pairs: - _key_ = your token key - _secret_ = your token secret It should look like: ![Rancher secret create](https://webhookrelay.com/blog/rancher-push-to-deploy-workflow/images/blog/rancher-push-to-deploy/rancher-secret-create.png) ### [Configure Webhook Relay bucket](#configure-webhook-relay-bucket) Buckets provide a grouping of your public endpoints and destinations, where to forward webhooks. Open [https://my.webhookrelay.com/buckets](https://my.webhookrelay.com/buckets) and create a new bucket named 'keel'. Once you have it, you will need to create a new output that will point to our Keel. Since Webhook Relay is a sidecar, we will reach it by localhost, so the destination is [http://localhost:9300/v1/webhooks/dockerhub](http://localhost:9300/v1/webhooks/dockerhub). Keel supports a number of registries, you can view their endpoints in the [official Keel documentation](https://keel.sh/v1/guide/documentation.html#Webhooks). ![Rancher Webhook Relay output create](https://webhookrelay.com/blog/rancher-push-to-deploy-workflow/images/blog/rancher-push-to-deploy/rancher-output-create.png) ### [Deploying Keel with Webhook Relay sidecar](#deploying-keel-with-webhook-relay-sidecar) Now, when in Keel project go to **Workloads** and click "import yaml". Take example configuration from [https://raw.githubusercontent.com/keel-hq/keel/master/deployment/deployment-rbac-whr-sidecar.yaml](https://raw.githubusercontent.com/keel-hq/keel/master/deployment/deployment-rbac-whr-sidecar.yaml). In this example we will just update: - Image tag to the current latest `0.13.0-rc1`. - In the Webhook Relay sidecar configuration set bucket to 'keel': ``` - name: BUCKET value: "keel" ``` > At this point you can configure more things in Keel such as Slack notifications, AWS ECR registry authentication or anything else that you think is useful. You should see Keel being created and happy: ![Rancher Keel created](https://webhookrelay.com/blog/rancher-push-to-deploy-workflow/images/blog/rancher-push-to-deploy/rancher-keel-created.png) ### [Configure DockerHub webhooks](#configure-dockerhub-webhooks) Head to your DockerHub repository [https://hub.docker.com/r/karolisr/webhook-demo/~/settings/webhooks/](https://hub.docker.com/r/karolisr/webhook-demo/~/settings/webhooks/) (replace with your username and repository) and add your public webhooks endpoint from [https://my.webhookrelay.com/buckets](https://my.webhookrelay.com/buckets): ![Dockerhub webhooks configuration](https://webhookrelay.com/blog/rancher-push-to-deploy-workflow/images/blog/rancher-push-to-deploy/rancher-dockerhub-config.png) In this step, we configure DockerHub to send notifications to Webhook Relay about push events to the Docker registry. ## [Deploying example application](#deploying-example-application) First, we will deploy our example app, image: `karolisr/webhook-demo:0.0.15`: ![Rancher configure app](https://webhookrelay.com/blog/rancher-push-to-deploy-workflow/images/blog/rancher-push-to-deploy/rancher-keel-demo-deploy.png) Don't set Labels or Annotations through the Rancher's UI as they will be set to the pod spec instead of the deployment. If they are in the pod spec - Keel will not detect them! Deploy it and then from the workload options select 'View/Edit YAML': ![Rancher edit app yaml](https://webhookrelay.com/blog/rancher-push-to-deploy-workflow/images/blog/rancher-push-to-deploy/rancher-edit-yaml.png) Here, Rancher created a deployment specification for us but we will need to specify an update policy. Use either labels or annotations (Keel supports annotations from `0.13.0-rc1` version) to specify the policy: ``` apiVersion: apps/v1beta2 kind: Deployment metadata: annotations: deployment.kubernetes.io/revision: "1" field.cattle.io/creatorId: user-pxt5q keel.sh/policy: major # <- this one! ``` There are other policies available, such as: - Semver - `patch`, `minor`, `major`, `all`. - Glob - `glob:build-*` will update when new tags such as `build-10`, `build-11` are created where `*` can be anywhere in the tag. - Regex - `regexp:^([a-zA-Z]+)$`. ## [Pushing new version](#pushing-new-version) In my case I have a simple Makefile to push images: ``` TAG = 0.16.0 build: CGO_ENABLED=0 GOOS=linux go build -a -tags netgo -ldflags -'w' -o webhook-demo . image: build docker build -t karolisr/webhook-demo:$(TAG) -f Dockerfile . push: image docker push karolisr/webhook-demo:$(TAG) ``` After the push, we should see our workload updated: ![Rancher updated application](https://webhookrelay.com/blog/rancher-push-to-deploy-workflow/images/blog/rancher-push-to-deploy/rancher-updated.png) Also, in Webhook Relay [logs page](https://my.webhookrelay.com/logs?limit=10&offset=0) you can see the webhooks ## [A little FAQ](#a-little-faq) **Q: Can I set annotations/labels through the Rancher's web UI when I am creating a new workload?** A: Unfortunately those end up in a pod spec and not the deployment so Keel won't detect them and auto updates won't work. **Q: Do I always need to use Webhook Relay? ** A: Of course not, it just helps with scenarios where Keel is not reachable by the DockerHub or any other registry (private network, localhost) **Q: What happens if something breaks?** A: Keel is event driven so you can easily go to your Rancher's UI and do a rollback: ![Rancher rollback](https://webhookrelay.com/blog/rancher-push-to-deploy-workflow/images/blog/rancher-push-to-deploy/rancher-rollback.png) --- --- title: Home Assistant remote access add-on | WebhookRelay meta: "og: title": "Home Assistant remote access add-on" description: Reverse tunnels for testing and development environments url: https://webhookrelay.com/blog/hassio-tls-tunnels-duckdns.md file: /blog/hassio-tls-tunnels-duckdns.md --- ![Stripes](https://webhookrelay.com/blog/hassio-tls-tunnels-duckdns/images/stripes.svg) # **Home Assistant remote access add-on** Reverse tunnels for testing and development environments Webhook Relay [add-on](https://github.com/webhookrelay/home-assistant) allows remote access to Home Assistant without configuring your router, firewall or having a static public IP. It works by creating secure reverse tunnels back to the cloud service. ![Webhook Relay Add-on](https://webhookrelay.com/blog/hassio-tls-tunnels-duckdns/images/blog/hassio-addon/ha-duckdns.png) ## [Goal of this tutorial](#goal-of-this-tutorial) Secure, end-to-end TLS encrypted access to your Home Assistant without configuring your router or having a static IP. Instead of HTTPS tunnels that are being terminated on Webhook Relay servers, we will be using [TLS tunnels](https://docs.webhookrelay.com/products/tunnels/tls-tunnels#tls-pass-through) that are only being terminated at your end so even if we are forced to, we couldn't intercept traffic without your browser notifying you. ## [Before we begin](#before-we-begin) This tutorial will expect you to have: - **Basic** plan subscription ($9.99/m) which enables whitelisted domains and TLS pass-through tunnels. Check plans and pricing [here](https://webhookrelay.com/pricing). We do offer a 7-day money back guarantee. Subscribe to it on your [accounts](https://my.webhookrelay.com/account/plan) page. If you don't want to commit, drop us an email at [info@webhookrelay.com](https://webhookrelay.com/blog/hassio-tls-tunnels-duckdns//mailto:info@webhookrelay.com) and we will set up a trial for you. - [DuckDNS](https://www.duckdns.org/) account to get your own free domain and retrieve TLS certificate through [Let's Encrypt](https://letsencrypt.org/). ## [Quick Start](#quick-start) - Create Webhook Relay account [here](https://my.webhookrelay.com) - Create [DuckDNS](https://www.duckdns.org/) account: ![DuckDNS configuration](https://webhookrelay.com/blog/hassio-tls-tunnels-duckdns/images/blog/hassio-addon/duckdnsconfig.png) Installation of this add-on is pretty straightforward and not different in comparison to installing any other [Hass.io](https://hass.io) add-on: 1. Add our Hass.io add-ons repository URL to your Hass.io instance: [https://github.com/webhookrelay/home-assistant](https://github.com/webhookrelay/home-assistant). 2. Install the “Webhook Relay” add-on. 3. Generate [token key & secret pair](https://my.webhookrelay.com/tokens) and add it to the add-on configuration. 4. Get [DuckDNS](https://www.duckdns.org/) token and create your domain. Add those details to the "tunnels" config section and "duck_dns" section. Set "accept_terms" to true if you accept [Let's Encrypt ToS](https://community.letsencrypt.org/tos). 5. Start the “Webhook Relay” add-on. 6. Check the logs of the “Webhook Relay” add-on to see if everything went well. It should print out your public URL. ### [TLS pass-through add-on configuration](#tls-pass-through-add-on-configuration) Once you have: - Webhook Relay key & secret - DuckDNS domain - DuckDNS token Use those details to populate add-on configuration: ``` { "key": "your-webhookrelay-key", "secret": "your-webhookrelay-secret", "forwarding": [ { "bucket": "ha", "destination": "http://127.0.0.1:8123" } ], "tunnels": [ { "name": "ha", "destination": "http://127.0.0.1:8123", "protocol": "tls", "domain": "your-domain.duckdns.org" } ], "duck_dns": { "token": "your-duckdns-token", "accept_terms": true }, "tunnels_enabled": true, "forwarding_enabled": false } ``` Make sure that the "protocol" is set to `tls`, `tunnels_enabled` is set to **true**. Add-on will automatically: 1. Configure DuckDNS to point to a correct address (public tunnel endpoint IP) 2. Configure and retrieve certificate and private key from Let's Encrypt 3. Store certificate & key in `/data/` directory 4. Configure Webhook Relay tunnel 5. Starts serving traffic ### [Wrapping up](#wrapping-up) That's it, you should be able to access your Home Assistant through your domain that you have configured, in my case it's [https://auto-ha.duckdns.org](https://auto-ha.duckdns.org). We have got full, end-to-end encryption without configuring your router or getting a static IP: ![Remote Access](https://webhookrelay.com/blog/hassio-tls-tunnels-duckdns/images/blog/hassio-addon/domain-ss.png) Also, instead of using DuckDNS & Let's Encrypt, you can use any certificate you want or just don't supply any certs to the add-on and terminate TLS on Home Assistant server. For that you will just have to specify HTTPS in the destination: `"destination": "https://127.0.0.1:8123"` ### [P.S.](#ps) If you just want to receive webhooks, feel free to use our free tier! Or if you don't want to commit but would really like to try it, email us at [info@webhookrelay.com](https://webhookrelay.com/blog/hassio-tls-tunnels-duckdns//mailto:info@webhookrelay.com) and we might think of something :) Not using Hass.io? Check out my previous blog post that details a simple setup with Docker [here](https://webhookrelay.com/blog/hassio-tls-tunnels-duckdns/blog/2018/09/03/home-assistant-remote-access/). --- --- title: Documenting your API with OpenAPI (Swagger) and Redoc meta: "og: title": "Documenting your API with OpenAPI (Swagger) and Redoc" description: API tooling review and a guide on how to document your API with Swagger's OpenAPI and Redoc url: https://webhookrelay.com/blog/openapi-redoc-tutorial.md file: /blog/openapi-redoc-tutorial.md --- ![Stripes](https://webhookrelay.com/blog/openapi-redoc-tutorial/images/stripes.svg) # **Documenting your API with OpenAPI (Swagger) and Redoc** API tooling review and a guide on how to document your API with Swagger's OpenAPI and Redoc ## [What is Redoc?](#what-is-redoc) Redoc is an open-source tool that generates beautiful, responsive API documentation from OpenAPI (Swagger) specifications. It provides a three-panel layout with a navigation menu, detailed descriptions, and interactive code samples, making it easy to create professional API documentation that can be embedded in any website. In this article, we will review several popular editors suitable for documenting APIs with the OpenAPI 3.0 specification, different themes that can render the spec, as well as hosting strategies. ## [TL;DR](#tldr) Best combination that we found: - Use [OpenAPI 3.0](https://www.openapis.org/) - Implement specification with [VSCode Swagger Extension](https://github.com/arjun-g/vs-swagger-viewer) - Use GitHub pages with [Redoc](https://github.com/Rebilly/ReDoc) for a good looking & free hosting ## [Choosing API specification format](#choosing-api-specification-format) I guess it’s safe to say that OpenAPI is now the most popular API specification out there. You can find out more about it here: [https://www.openapis.org/](https://www.openapis.org/). Other contenders: - RAML - API Blueprint - WADL - Slate You can read more about other top specification formats on an excellent Nordic APIs article here: [https://nordicapis.com/top-specification-formats-for-rest-apis/](https://nordicapis.com/top-specification-formats-for-rest-apis/) ## [OpenAPI vs/and Swagger](#openapi-vsand-swagger) Short history: OpenAPI 3.0 was the first official release since it was donated to the OpenAPI initiate by the [SmartBear Software](https://smartbear.com/) (and renamed from the Swagger Specification). A lot of people still think (myself included before I did some research) that Swagger is still a specification, however, currently: - OpenAPI is a specification - Swagger provides tools for writing specification, generating code & hosting it. In the past years, OpenAPI has been embraced by major enterprises and startups of various sizes. ## [Editors](#editors) Once you have a specification chosen, it’s important to look for a good way to actually write it down. I initially started with [https://apiary.io](https://apiary.io) as they offer an editor with Swagger 2.0 and API Blueprint options (defaults to API Blueprint so watch out:) as well as hosting your documentation on their service: ![Apiary editor](https://webhookrelay.com/blog/openapi-redoc-tutorial/images/blog/openapi-redoc-guide/apiary-editor.png) Sounds like a good deal? It probably is, since it offers an all-in-one package - editor, syntax check and even hosts your docs for free. Let's have a look at other options :) ### [Swagger editor](#swagger-editor) I then looked into Swagger editor ([https://editor.swagger.io/](https://editor.swagger.io/)) but I opted for a self-hosted via Docker: ![Swagger editor](https://webhookrelay.com/blog/openapi-redoc-tutorial/images/blog/openapi-redoc-guide/swagger-editor.png) It’s very similar to apiary.io offering but the main problem I found with both of them was that they are just not as fast as my editor that I use locally. ### [Swagger VSCode extension](#swagger-vscode-extension) Naturally, I checked out VSCode extensions marketplace and found this excellent piece [https://github.com/arjun-g/vs-swagger-viewer](https://github.com/arjun-g/vs-swagger-viewer): ![Swagger editor VSCode](https://webhookrelay.com/blog/openapi-redoc-tutorial/images/blog/openapi-redoc-guide/swagger-vscode.png) With the extension you get: - autocompletion - all your shortcuts work - integrated Git All in all, while I started documenting API in Apiary, by switching to VSCode extension it greatly improved the speed at which I could document. ## [Hosting your documentation](#hosting-your-documentation) While I really enjoy both Swagger 2.0 and OpenAPI specification format, swagger docs weren’t particularly visually attractive to me. Apiary does offer a nice theme: ![Apiary docs](https://webhookrelay.com/blog/openapi-redoc-tutorial/images/blog/openapi-redoc-guide/apiary-docs.png) And would probably be my first choice of hosting if we didn't already have a website where we host docs. But at the end of the day, it’s just a single page and having a 3rd party hosting dependency was a bit too much. ## [Enter ReDoc](#enter-redoc) After spending a bit of time looking at various themes and tools, I found my favorite - ReDoc ([https://github.com/Rebilly/ReDoc](https://github.com/Rebilly/ReDoc).) It offers an incredibly nice theme, the project is active and very customizable. The main reason I chose ReDoc was because of how easy it is to embed documentation with it. ## [How to Set Up Redoc Documentation](#how-to-set-up-redoc-documentation) Follow these steps to set up Redoc for your API documentation: 1. **Create Your OpenAPI Specification** - Write your API documentation in OpenAPI 3.0 or Swagger 2.0 format using a YAML or JSON file 2. **Host Your Specification File** - Put your openapi.yaml or swagger.yaml in a publicly accessible place (GitHub repo, CDN, or your own server) 3. **Add Redoc Script** - Include the Redoc standalone JavaScript bundle in your HTML page 4. **Initialize Redoc** - Call Redoc.init() with your specification URL and optional configuration 5. **Customize Appearance** - Configure options like scroll offset, theme colors, and button visibility 6. **Deploy Your Documentation** - Host your documentation page on GitHub Pages, Netlify, or your own web server ### [Example Implementation](#example-implementation) Here's how to embed Redoc in your website: 1. Put your openapi.yaml or swagger.yaml in a publicly accessible place. In our case I have put it in a Git repo: [https://github.com/webhookrelay/swagger-webhookrelay/blob/master/openapi.yaml](https://github.com/webhookrelay/swagger-webhookrelay/blob/master/openapi.yaml) Storing it in a git repo offers a nice feature - you edit, push it and it always stays up-to-date without redeploying your website. 2. In your website add these lines: ``` ``` And that's it, your API reference is now hosted: ![Redoc Webhook Relay API reference](https://webhookrelay.com/blog/openapi-redoc-tutorial/images/blog/openapi-redoc-guide/redoc-whr.png) ## [API Documentation Tools Comparison](#api-documentation-tools-comparison) Here's how Redoc compares to other popular API documentation tools: | Feature | Redoc | Swagger UI | Apiary | Slate | | --- | --- | --- | --- | --- | | **OpenAPI Support** | ✓ 3.0 & 2.0 | ✓ 3.0 & 2.0 | ✓ 2.0 only | ✗ Markdown | | **Visual Design** | Modern 3-panel | Classic 2-panel | Modern | Developer-focused | | **Customization** | High | Medium | Low | High | | **Hosting** | Self-hosted/CDN | Self-hosted/CDN | Cloud-hosted | Self-hosted | | **Interactive Examples** | ✓ Yes | ✓ Try it out | ✓ Console | ✓ Yes | | **Open Source** | ✓ Yes | ✓ Yes | ✗ No | ✓ Yes | | **Ease of Setup** | Very Easy | Easy | Very Easy | Moderate | | **Cost** | Free | Free | Paid plans | Free | Redoc stands out for its beautiful design, ease of embedding, and excellent OpenAPI 3.0 support. It's ideal if you want professional-looking documentation without complex setup. ## [Conclusion](#conclusion) Documenting your API can be quite fun if you pick the right tools that are not slowing you down. In our case we used a nice editor with features tailored to OpenAPI spec and publishing your API docs to the world can also be a pain-free experience. Obviously, suggested tools were only the best for me, I suggest that before you start documenting your own APIs you would also do a 30-minute research with some trials on different editors, themes and hosting. Maybe you will find some other combination that suits you better. At the end of the day, choosing the right tooling will save you a lot of time :) good luck! Worst case scenario - once the specification is done, it's easy to try out different themes and hosting options as well. > Bonus: Swagger 2.0 to OpenAPI 3.0 converter: [https://openapi-converter.herokuapp.com/](https://openapi-converter.herokuapp.com/) ### [Reference](#reference) - [https://trends.google.com/trends/explore?cat=13&q=swagger,openapi,raml](https://trends.google.com/trends/explore?cat=13&q=swagger,openapi,raml) - [https://swagger.io/blog/api-strategy/difference-between-swagger-and-openapi/](https://swagger.io/blog/api-strategy/difference-between-swagger-and-openapi/) - [https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#oasDocument](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#oasDocument) - [https://nordicapis.com/top-specification-formats-for-rest-apis/](https://nordicapis.com/top-specification-formats-for-rest-apis/) - [https://raml.org/](https://raml.org/) - [https://github.com/Rebilly/ReDoc](https://github.com/Rebilly/ReDoc) --- --- title: Home Assistant Remote Access on Raspberry Pi | WebhookRelay meta: "og: title": "Home Assistant Remote Access on Raspberry Pi" description: How to connect to your Home Assistant without public IP or NAT configuration url: https://webhookrelay.com/blog/home-assistant-remote-access.md file: /blog/home-assistant-remote-access.md --- ![Stripes](https://webhookrelay.com/blog/home-assistant-remote-access/images/stripes.svg) # **Hassle-free remote access to Home Assistant on a Raspberry Pi** How to connect to your Home Assistant without public IP or NAT configuration ![Home Assistant & Webhook Relay](https://webhookrelay.com/blog/home-assistant-remote-access/images/blog/home-assistant/header.png) There are quite a few home automation systems out there but one of my favorites is [Home Assistant](https://www.home-assistant.io/). It's written in Python ([available on GitHub](https://github.com/home-assistant/home-assistant)), has a nice user interface and is very easy to deploy. Home Assistant has tons of integrations with many other online services and hardware devices. In this short article we will use: - [Raspbian](https://www.raspberrypi.org/downloads/) - operating system - [Etcher](https://etcher.io/) - probably the best tool to burn images - [Raspberry PI](https://www.raspberrypi.org/products/) - our mini computer :) - [Docker](https://www.docker.com/) - the easiest way to package and run server applications - [Webhook Relay](https://webhookrelay.com/) - tunneling service to access Home Assistant from outside - [Home Assistant](https://www.home-assistant.io/) - home automation system > Using Hass.io? Check out our Add-on [here](https://webhookrelay.com/blog/home-assistant-remote-access/blog/2018/10/12/hassio-tls-tunnels-duckdns/). ## [TL;DR](#tldr) Create tunnel through the [UI](https://my.webhookrelay.com/tunnels) and provision an access key & secret [from the tunnels page](https://my.webhookrelay.com/tokens). Having those details, launch **webhookrelayd** container in a tunnel mode with your tunnel name specified (in this case tunnel name is `rpi`): ``` docker run --name whr-relayd --net host --restart always -d webhookrelay/webhookrelayd-arm:latest --mode tunnel -t rpi -k token-key -s token-secret ``` Start Home Assistant ``` docker run -d --name assistant -v /home/pi/home_assistant:/config -v /etc/localtime:/etc/localtime:ro --restart always homeassistant/raspberrypi3-homeassistant:0.76.2 ``` Open tunnel in the browser `your-subdomain.webrelay.io` from anywhere: ![Home assistant](https://webhookrelay.com/blog/home-assistant-remote-access/images/blog/home-assistant/ui.png) ## [The detailed version](#the-detailed-version) ### [Memory Card Preparation:](#memory-card-preparation) - Insert MicroSD memory card into a computer - Format it - Download the Raspbian OS image from [https://www.raspberrypi.org/downloads/raspbian/](https://www.raspberrypi.org/downloads/raspbian/) - Open [Etcher](https://etcher.io/) ![Etcher]](/images/blog/home-assistant/etcher.png) - Using Etcher select the downloaded Raspbian image - Select the drive that corresponds to your memory card - Click flash! ### [Install Docker](#install-docker) From the Raspbian Desktop launch Terminal. Now, using terminal install Docker: ``` curl -sSL https://get.docker.com | sh ``` Official blog post on Docker support for Raspberry Pi can be found [on their website](https://www.raspberrypi.org/blog/docker-comes-to-raspberry-pi/). Now, reboot it: ``` sudo reboot ``` ### [Install Home Assistant](#install-home-assistant) Visit the Raspberry Pi 3 Home Assistant Repository on the Docker Hub to determine the latest version available. To start Home Assistant is as simple as: ``` docker run -d --name assistant --net host -v /home/pi/home_assistant:/config -v /etc/localtime:/etc/localtime:ro --restart always homeassistant/raspberrypi3-homeassistant:0.76.2 ``` ### [Create a tunnel & token](#create-a-tunnel-token) Open [https://my.webhookrelay.com/tunnels](https://my.webhookrelay.com/tunnels) in your browser and click "create tunnel". If you are on a free plan, leave 'subdomain' and 'crypto' fields empty as they are only available for the paid plans, you will get auto-generated subdomain. ![Create tunnel]](/images/blog/home-assistant/rpi-tunnel.png) We will also need a token for authentication. Go to [https://my.webhookrelay.com/tokens](https://my.webhookrelay.com/tokens) and create a new token key & secret pair. Keep secret somewhere safe as it is now encrypted and cannot be recovered. If you lose it, just generated a new pair. ### [Start webhookrelayd](#start-webhookrelayd) To start a tunneling daemon, run (just replace key and secret with your own): ``` docker run --name whr-relayd --net host --restart always -d webhookrelay/webhookrelayd-arm:latest --mode tunnel -t rpi -k your-key -s your-secret ``` ## [Conclusion](#conclusion) That's it, now you can access your Home Assistant remotely, without any need to configure your router, get static IP or buying a domain. Tunnels can also be protected by a basic auth but it would be better if you just enabled authentication for your Home Assistant. Check for more information in the official [docs](https://www.home-assistant.io/components/http/). --- --- title: DevOps Use Case: Performing Redis maintenance in Kubernetes meta: "og: title": "DevOps Use Case: Performing Redis maintenance in Kubernetes" description: Use Redis-Commander with Webhook Relay ingress controller to access Redis in a Kubernetes cluster url: https://webhookrelay.com/blog/kubernetes-redis-commander.md file: /blog/kubernetes-redis-commander.md --- ![Stripes](https://webhookrelay.com/blog/kubernetes-redis-commander/images/stripes.svg) # **DevOps Use Case: Performing Redis maintenance in Kubernetes** Use Redis-Commander with Webhook Relay ingress controller to access Redis in a Kubernetes cluster Nowadays it's easy to run pretty much anything in Kubernetes clusters. But what about debugging these services? What if you need to quickly access a service that is normally not exposed to the internet or your intranet and is only accessible from within the cluster? Your service is like: ![no loadbalancer](https://webhookrelay.com/blog/kubernetes-redis-commander/images/blog/kubernetes-redis-commander/no-lb.jpg) In this short article, I will demonstrate how to connect to a running Redis instance with an excellent and powerful [Redis Commander](https://github.com/joeferner/redis-commander) GUI. ## [TL;DR](#tldr) - Deploy [Webhook Relay ingress controller](https://webhookrelay.com/blog/kubernetes-redis-commander/docs/installation/kubernetes#option-4-ingress-controller/) - Deploy [Redis Commander](https://github.com/webhookrelay/k8s-redis-commander/blob/master/redis-commander.yaml) - Access Redis Commander through Webhook Relay tunnel & debug your Redis node ## [Prerequisites](#prerequisites) - Kubernetes, you can either use an existing cluster that you have or use [Minikube](https://kubernetes.io/docs/setup/minikube/#installation) or Docker for Mac. - Webhook Relay account, sign up [here](https://my.webhookrelay.com/register) - [Webhook Relay CLI](https://docs.webhookrelay.com/installation-options/installation-options) ## [Action!](#action) Let's deploy Redis (skip if you already have it running), from the [cloned repository](https://github.com/webhookrelay/k8s-redis-commander) deploy it: ``` kubectl apply -f redis.yaml ``` It will create: - Redis deployment - Service that will make Redis accessible within the cluster Now, install our tunnel based ingress controller into your cluster: ``` relay ingress init ``` This command: - Creates a namespace - Create an authentication secret for the ingress controller to use your account - Deploy an actual ingress controller You can check whether it's running by: ``` $ kubectl get pods -n webrelay-ingress NAME READY STATUS RESTARTS AGE webrelay-69996f8d7c-522z8 1/1 Running 0 10s ``` ### [Configure Redis Commander](#configure-redis-commander) Now, open `redis-commander.yaml` in your favorite code editor (mine is VSCode :) and edit several details. If you are on a free tier, unfortunately, you won't be able to choose a subdomain for your tunnel so you will need to create one first. Also, if you have chosen some different name for your Redis service, then update `REDIS_HOSTS` environment variable. To create a tunnel, run: ``` $ relay tunnel create --group webrelay-ingress hello-ingress 2p4ptkh9vutgm8tqavigja.webrelay.io<---->http://127.0.0.1 ``` Now, copy `2p4ptkh9vutgm8tqavigja.webrelay.io` into the last, ingress section and to replace `[REPLACE THIS WITH YOUR TUNNEL NAME]` ``` apiVersion: v1 kind: Service metadata: labels: name: redis-commander name: redis-commander spec: ports: - port: 8081 protocol: TCP targetPort: 8081 selector: app: redis-commander type: NodePort --- apiVersion: apps/v1 kind: Deployment metadata: name: redis-commander spec: replicas: 1 selector: matchLabels: app: redis-commander template: metadata: labels: app: redis-commander tier: backend spec: containers: - name: redis-commander image: rediscommander/redis-commander:latest env: - name: REDIS_HOSTS value: redis:redis:6379 - name: REDIS_PORT value: "" ports: - name: redis-commander containerPort: 8081 --- apiVersion: extensions/v1beta1 kind: Ingress metadata: annotations: kubernetes.io/ingress.class: webrelay # other ingress classes will be ignored name: relay-ingress spec: rules: - host: [REPLACE THIS WITH YOUR TUNNEL NAME] http: paths: - path: / backend: serviceName: redis-commander servicePort: 8081 ``` Once you have finished editing, create it: ``` kubectl apply -f redis-commander.yaml ``` It should now be accessible from your browser: ![Redis commander](https://webhookrelay.com/blog/kubernetes-redis-commander/images/blog/kubernetes-redis-commander/redis-commander.png) > Cool, yeah? Good luck with your other experiments! ![cool? yeah](https://webhookrelay.com/blog/kubernetes-redis-commander/images/blog/kubernetes-redis-commander/batman.gif) ## [./wrap_up](#wrap_up) To wrap up, the same strategy can be applied to other services like Prometheus or Grafana. You can create tunnels only when you need them, for example, while Grafana can always be connected, you would only want to have a look at Prometheus when you are not sure if service discovery is missing something or want to use raw queries. --- --- title: Keel - automated Kubernetes updates | WebhookRelay meta: "og: title": "Keel - automated Kubernetes updates" description: Automatically update kubernetes deployments on image push url: https://webhookrelay.com/blog/introducing-keel.md file: /blog/introducing-keel.md --- ![Stripes](https://webhookrelay.com/blog/introducing-keel/images/stripes.svg) # **Keel - automated Kubernetes updates** Automatically update kubernetes deployments on image push ![keel overview](https://webhookrelay.com/blog/introducing-keel/images/keel-overview.png) ## [Synopsis](#synopsis) If you are using Kubernetes for dev/test/production - you need a way to automate deployment updates once new images are available. Keel is a lightweight service to take care of that. You can find more on its website: [https://keel.sh](https://keel.sh) and Github repository [https://github.com/keel-hq/keel](https://github.com/keel-hq/keel). --- While [Container Builder](https://cloud.google.com/container-builder/docs/) and [Google Container Engine (Kubernetes)](https://cloud.google.com/container-engine/) make a great pair and building images and running your workloads - there is a missing gap: who/what updates deployments when new images are available? maybe it's you: 1. update image tag in `deployment.yaml` 2. run `kubectl apply -f deployment.yaml` It does feel good to do that rolling update for the first few times, but what if you don't have access to _kubectl_ or simply you are making lots of releases? These updates feel too repetitive and simply not necessary. This is what Keel solves: pluggable trigger system (webhooks, pubsub, polling) and pluggable provider system (Kubernetes, Helm). ## [Keel overview](#keel-overview) Keel acts as a native Kubernetes service, main features: - Runs silently and doesn't require direct interactions from the user (users label deployments that are eligible for updates) - Automatically creates topic & subscriptions for your GCR images (GCR uses pubsub instead of webhooks to notify regarding push/delete events in registry) so you don't have to - Accepts webhooks from DockerHub, Quay, JFrog (because not everyone runs on Google Cloud) - Schedules regular image SHA digest checks if you don't have access to webhooks (ie: repository is not yours) and scans for new tags So, once you have deployed Keel in your Kubernetes cluster, the workflow looks like this: 1. Tag a release in GitHub 2. Cloudbuild/{insert your favorite builder here} starts building an image 3. Keel gets new image event, looks for impacted deployments marked with keel update policy and starts rolling update Easy! Sounds like a PaaS, right? If you have any questions - you can often find me on k8s Slack channel: [kubernetes.slack.com](https://kubernetes.slack.com) look for @karolis --- --- title: Web Relay Ingress with Docker for Mac | WebhookRelay meta: "og: title": "Web Relay Ingress with Docker for Mac" description: Web Relay ingress for Mac lets users expose their local services to the internet for testing and demoing url: https://webhookrelay.com/blog/ingress-with-docker-for-mac.md file: /blog/ingress-with-docker-for-mac.md --- ![Stripes](https://webhookrelay.com/blog/ingress-with-docker-for-mac/images/stripes.svg) # **Web Relay Ingress with Docker for Mac** Web Relay ingress for Mac lets users expose their local services to the internet for testing and demoing ![Docker for Mac now supports Kubernetes](https://webhookrelay.com/blog/ingress-with-docker-for-mac/images/blog/ingress-with-docker-for-mac/docker-for-mac-mashup.png) Kubernetes became available in **Docker for Mac 17.12 CE Edge**. [Kubernetes](https://kubernetes.io/) in the last year showed that it's the most flexible and **reliable** option to run container workloads, all the major cloud providers now offer or are planning to offer a managed Kubernetes service to their customers: - [https://cloud.google.com/kubernetes-engine](https://cloud.google.com/kubernetes-engine) - [https://azure.microsoft.com/en-us/services/container-service](https://azure.microsoft.com/en-us/services/container-service) - [https://aws.amazon.com/eks](https://aws.amazon.com/eks) - [https://www.ibm.com/cloud/container-service](https://www.ibm.com/cloud/container-service) And some great companies that help you deploy and run your own cluster: - [https://www.ankyra.io/](https://www.ankyra.io/) - [https://stackpoint.io/](https://stackpoint.io/) - [https://heptio.com/](https://heptio.com/) - [https://appscode.com/](https://appscode.com/) After visiting last KubeCon in Austin I have seen a huge increase in the number of companies that specialize in Kubernetes consulting. This list could go on and on :) **In this article we will:** - Enable Kubernetes support in your **Docker for Mac**. - Create and deploy an example Node.js application. - Use Web Relay ingress controller to share that app running inside our Mac to the world :) ![Ingress with Docker for Mac](https://webhookrelay.com/blog/ingress-with-docker-for-mac/images/blog/ingress-with-docker-for-mac/docker-for-mac.gif) **Prerequisites:** - Docker for Mac **17.12 CE Edge**. - Webhook Relay [account](https://my.webhookrelay.com/register) and [relay](https://webhookrelay.com/blog/ingress-with-docker-for-mac/docs/installation/cli/) client command. - `kubectl`, the Kubernetes client command. It should be included and configured by the Docker for Mac. > If you are not using Mac or **Docker for Mac** you can still follow this tutorial step-by-step, just skip the "Enable Kubernetes in your Docker for Mac" section. This tutorial will work for ANY Kubernetes cluster as long as it has an Internet connectivity. ## [Getting started](#getting-started) Time to get our hands dirty! Feel free to skip a few things like enabling Kubernetes if you have already done it. ### [Enable Kubernetes in your Docker for Mac](#enable-kubernetes-in-your-docker-for-mac) To enable Kubernetes support inside your Docker for Mac, select **Enable Kubernetes** and click the **Apply and restart** button: ![Kubernetes in Docker for Mac](https://webhookrelay.com/blog/ingress-with-docker-for-mac/images/blog/ingress-with-docker-for-mac/kubernetes-enable.png) It should take a bit of time depending on the available Internet bandwidth and once it is done, it should report that the installation is complete. If you have any problems with this step, it might make sense to visit [Docker documentation](https://docs.docker.com/docker-for-mac/#kubernetes) on this matter. Unlike Minikube, Docker for Mac doesn't hijack kubectl context, so you have to set it: ``` kubectl config use-context docker-for-desktop ``` Check whether your `kubectl` is using the **docker-for-desktop** context. ``` $ kubectl config get-contexts CURRENT NAME CLUSTER AUTHINFO NAMESPACE minikube minikube minikube * docker-for-desktop docker-for-desktop-cluster docker-for-desktop ``` ### [Add ingress controller to your Docker for Mac Kubernetes](#add-ingress-controller-to-your-docker-for-mac-kubernetes) Add ingress controller: ``` relay ingress init ``` Output: ``` $ relay ingress init using manifest from 'https://raw.githubusercontent.com/webrelay/ingress/master/deployment/deployment-rbac.yaml'... namespace "webrelay-ingress" created serviceaccount "webrelay" created deployment "webrelay" created clusterrolebinding "webrelay" created clusterrole "webrelay" created ingress added to the cluster, configuring authentication... key and secret not supplied, generating new access credentials... secret "webrelay-credentials" created ``` Yeah, some of you are probably now like: ![Back in my day we used helm](https://webhookrelay.com/blog/ingress-with-docker-for-mac/images/blog/ingress-with-docker-for-mac/back-in-my-day-helm.png) But hold on, this command creates RBAC-enabled ingress controller in `webrelay-ingress` namespace with already configured credentials for **your** account. You can read more about Web Relay ingress controller [here](https://webhookrelay.com/blog/ingress-with-docker-for-mac/docs/installation/kubernetes/). ### [Create your Node.js application](#create-your-nodejs-application) The next step is to write the application. Create a new directory named `hello` with the filename `server.js`: ``` var http = require('http'); var port = 8080 var handleRequest = function(request, response) { console.log('Received request for URL: ' + request.url); response.writeHead(200); response.end('Hello Internet!'); }; var www = http.createServer(handleRequest); console.log('Listening on http://localhost:' + port); www.listen(port); ``` To run the application: ``` node server.js ``` You should be able to see your “Hello World!” message at [http://localhost:8080/](http://localhost:8080/). Stop the running Node.js server by pressing **Ctrl-C**. The next step is to package your application in a Docker container. ### [Put your application into a Docker container](#put-your-application-into-a-docker-container) Create a file, also in the `hello` directory, named Dockerfile. A Dockerfile describes the image that you want to build. You can build a Docker container image by extending an existing image. The image in this tutorial extends an existing Node.js image. ``` FROM node:9.2.0 EXPOSE 8080 COPY server.js . CMD node server.js ``` This Dockerfile starts from the official Node.js image found in the Docker registry, exposes port 8080, copies your server.js file to the image and starts the Node.js server. Build your Docker image: ``` docker build -t hello-node:v1 . ``` What is really nice about Docker for Mac with Kubernetes is that you can easily run locally built Docker images inside Kubernetes cluster. No need to change Docker daemons or push images to the public repositories just to test them out. Now Docker for Mac Kubernetes can run the image you built. ### [Create Deployment and Service](#create-deployment-and-service) Kubernetes [Deployments](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/) checks on the health of the [Pods](https://kubernetes.io/docs/concepts/workloads/pods/pod/) and restarts the Pod's container if it terminates. Pod can consist of more than one containers but in this example we will only have one: ``` kubectl run hello-node --image=hello-node:v1 --port=8080 ``` To view Deployments: ``` kubectl get deployments ``` Output: ``` $ kubectl get deployments NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE hello-node 1 1 1 1 8s ``` Since by default Pod is only accessible by its internal IP address within the Kubernetes cluster, we need to expose it. Create a [Service](https://kubernetes.io/docs/concepts/services-networking/service/): ``` kubectl expose deployment hello-node --type=ClusterIP ``` To view created services: ``` kubectl get svc ``` Output: ``` $ kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE hello-node ClusterIP 10.102.194.95 8080/TCP 1m ``` ### [Define ingress and expose your app to the Internet](#define-ingress-and-expose-your-app-to-the-internet) ![Things get interesting](https://webhookrelay.com/blog/ingress-with-docker-for-mac/images/blog/ingress-with-docker-for-mac/things-get-interesting.png) Ingresses in Kubernetes provide an easy way to define routing rules between hostnames and services. Let's get our public endpoint with `relay` command client by creating a tunnel: ``` relay tunnel create --group webrelay-ingress hellonode ``` Output: ``` $ relay tunnel create --group webrelay-ingress hellonode pis9izc72c1wd9i21gxqxm.webrelay.io<---->http://127.0.0.1 ``` Parameter _--group webrelay-ingress_ is required to let our ingress controller know which tunnels it can manage. > Note that users with paid plans can specify any custom subdomain (as long as it is not taken) without creating a tunnel first. This allows to just easily define `ingress.yaml` and ingress controller wil create a tunnel for it. We are only interested in this `pis9izc72c1wd9i21gxqxm.webrelay.io` (host) part. Every user gets a unique link to their tunnel. Edit this code with your tunnel hostname and save it in a folder named `hello` with the filename `ingress.yml`: ``` # ingress.yml apiVersion: extensions/v1beta1 kind: Ingress metadata: annotations: kubernetes.io/ingress.class: webrelay # other ingress classes will be ignored name: relay-ingress namespace: default spec: rules: - host: pis9izc72c1wd9i21gxqxm.webrelay.io # <- host has to match tunnel host http: paths: - path: / backend: serviceName: hello-node servicePort: 8080 ``` Create it: ``` kubectl create -f ingress.yml ``` Output: ``` $ kubectl create -f ingress.yml ingress "relay-ingress" created ``` Your application is now exposed to the public Internet. See next section how to access or just enter that hostname into your browser. ### [Accessing your application through the Internet](#accessing-your-application-through-the-internet) To view existing ingresses: ``` relay ingress ls ``` Output: ``` relay ingress ls ID NAME HOST BACKENDS CRYPTO AUTH AGE 58f26c61-8e42-45f4-a982-4cb70990d7e2 webrelay-ingress-pis9izc72c1wd9i21gxqxm.webrelay.io pis9izc72c1wd9i21gxqxm.webrelay.io default/hello-node/8080 off - 2 seconds ``` **Backends** column should show `//` of the exposed service. You can access [http://pis9izc72c1wd9i21gxqxm.webrelay.io](http://pis9izc72c1wd9i21gxqxm.webrelay.io) with your browser (just change the link to your own tunnel address). You can also use web UI at [https://my.webhookrelay.com/tunnels](https://my.webhookrelay.com/tunnels) to view your ingresses: ![Ingress table in web UI](https://webhookrelay.com/blog/ingress-with-docker-for-mac/images/blog/ingress-with-docker-for-mac/ingress-table.png) ## [Wrapping up](#wrapping-up) In this article we created, deployed and exposed an app to the Internet that is running locally on our laptops. Some people say that this is an **"actual hello world"** application and not just the usual **"hello localhost"**. With Docker for Mac Kubernetes support it's now a lot easier to develop and test our applications. Build locally, run locally and demo locally. I hope Web Relay ingress controller will serve you great in developing, testing and running your apps. As always, if you have any questions, feel free to contact me. --- --- title: Auto deploy your Node.js app on push to GitHub meta: "og: title": "Auto deploy your Node.js app on push to GitHub" description: Learn how to update your Node.js app on push to GitHub using webhooks on any virtual machine or your local computer url: https://webhookrelay.com/blog/auto-deploy-on-git-push.md file: /blog/auto-deploy-on-git-push.md --- ![Stripes](https://webhookrelay.com/blog/auto-deploy-on-git-push/images/stripes.svg) # **Auto deploy your Node.js app on push to GitHub** Learn how to update your Node.js app on push to GitHub using webhooks on any virtual machine or your local computer Simple use case - deploy your updated Node.js app on a push to a GitHub repository. To achieve this, we will use several tools: - [nodemon](https://github.com/remy/nodemon) - monitor for any changes in your node.js application and automatically restart the server - perfect for development but we will make it perfect for production too :) - [webhook](https://github.com/adnanh/webhook) - incoming webhook server which can execute shell commands. - [relay](https://webhookrelay.com) - will allow us to receive webhooks anywhere without exposing them to the internet. ![workflow](https://webhookrelay.com/blog/auto-deploy-on-git-push/images/blog/update-on-git-push/push-to-update.png) All source code with example Node.js server and configuration can be found here: [https://github.com/webhookrelay/webhook-autoupdate](https://github.com/webhookrelay/webhook-autoupdate). ## [Preparing tools](#preparing-tools) First things first, let's clone the repository: ``` git clone https://github.com/webhookrelay/webhook-autoupdate cd webhook-autoupdate ``` Now, let's get the tooling. ### [1. Downloading and starting script executor](#_1-downloading-and-starting-script-executor) Now, install [webhook](https://github.com/adnanh/webhook). If you have a working Go environment, you can just do `go get github.com/adnanh/webhook`, otherwise go to the [https://github.com/adnanh/webhook/releases](https://github.com/adnanh/webhook/releases) page and grab the one that suits your operating system. If you have a casual Linux machine, you probably want _webhook-linux-amd64.tar.gz_ while MacOS users should choose _webhook-darwin-amd64.tar.gz_. Download, extract and put it in your PATH or just this repository. My [webhook-autoupdate](https://github.com/webhookrelay/webhook-autoupdate) repository holds `hooks.json` configuration file which should be supplied to the `webhook` app. First, edit the hooks JSON file with the correct path to command and working directory. Current one is ``` [ { "id": "webhook", "execute-command": "/home/karolis/go/src/github.com/webhookrelay/webhook-autoupdate/update.sh", // <- update this one "command-working-directory": "/home/karolis/go/src/github.com/webhookrelay/webhook-autoupdate", // <- update this one "pass-arguments-to-command": [ { "source": "payload", "name": "head_commit.id" }, { "source": "payload", "name": "pusher.name" }, { "source": "payload", "name": "pusher.email" } ], "trigger-rule": { "and": [ { "match": { "type": "payload-hash-sha1", "secret": "verysecret", // <- this has to match with your GitHub webhook secret "parameter": { "source": "header", "name": "X-Hub-Signature" } } }, { "match": { "type": "value", "value": "refs/heads/master", "parameter": { "source": "payload", "name": "ref" } } } ] } } ] ``` Update marked fields to something that reflects the path to where you have currently cloned this repository. Once you did that, start it: ``` $ webhook -hooks hooks.json -verbose [webhook] 2018/07/15 22:38:47 version 2.6.8 starting [webhook] 2018/07/15 22:38:47 setting up os signal watcher [webhook] 2018/07/15 22:38:47 attempting to load hooks from hooks.json [webhook] 2018/07/15 22:38:47 os signal watcher ready [webhook] 2018/07/15 22:38:47 found 1 hook(s) in file [webhook] 2018/07/15 22:38:47 loaded: webhook [webhook] 2018/07/15 22:38:47 serving hooks on http://0.0.0.0:9000/hooks/{id} ``` ### [2. Download and run relay agent](#_2-download-and-run-relay-agent) Start [relay](https://docs.webhookrelay.com/installation-options/installation-options/install-cli) agent: ``` $ relay forward -b exec http://localhost:9000/webhook Forwarding: https://my.webhookrelay.com/v1/webhooks/cde35b07-4f59-4bc7-8c0d-0846bf1e1800 -> http://localhost:9000/webhook Starting webhook relay agent... ``` Grab that public endpoint and head to your GitHub repository settings page: ![github settings](https://webhookrelay.com/blog/auto-deploy-on-git-push/images/blog/update-on-git-push/github-settings.png) Then, add public Webhook Relay endpoint and set "secret" the same one as in your hooks.json (in example file it's `"secret": "verysecret"`) and content type `application/json`: ![set webhook destination](https://webhookrelay.com/blog/auto-deploy-on-git-push/images/blog/update-on-git-push/github-add-webhook.png) ### [3. Start nodemon](#_3-start-nodemon) While in the repository, install dependencies: ``` npm install ``` Time to start our node app: ``` npm run nodemon > webhook-exec@1.0.0 nodemon /home/karolis/go/src/github.com/webhookrelay/webhook-autoupdate > nodemon server.js [nodemon] 1.18.2 [nodemon] reading config ./nodemon.json [nodemon] to restart at any time, enter \`rs\` [nodemon] or send SIGHUP to 1960 to restart [nodemon] ignoring: ./.git/**/* node_modules/**/node_modules [nodemon] watching: server.js [nodemon] watching extensions: js,json [nodemon] bind restart -> \`osascript -e 'display notification "App restarted due to: '$FILENAME'" with title "nodemon"'\` [nodemon] starting \`node --harmony server.js\` [nodemon] spawning [nodemon] child pid: 1974 [nodemon] watching 1 file Listening on http://localhost:8080 ``` That's it! If you push new changes to the GitHub repository, it will send a webhook that will trigger an update. ## [Trying it out](#trying-it-out) When you push to the repository, in `nodemon` terminal you should see: ``` [nodemon] files triggering change check: server.js [nodemon] matched rule: **/server.js [nodemon] changes after filters (before/after): 1/1 [nodemon] restarting due to changes... [nodemon] server.js sh: 1: osascript: not found [nodemon] starting \`node --harmony server.js\` [nodemon] spawning [nodemon] child pid: 3078 Listening on http://localhost:8080 ``` and in webhook relay bucket you should see request logs: ![bucket with logs](https://webhookrelay.com/blog/auto-deploy-on-git-push/images/blog/update-on-git-push/push-update-bucket.png) If you refresh the browser window [http://localhost:8080](http://localhost:8080), you will see the new code running. > Interested or working with webhooks? Check out how you can receive webhook on localhost or private networks in our website's [blog](https://webhookrelay.com/blog/auto-deploy-on-git-push/blog/). Webhook Relay has a free tier for developers! --- --- title: Introduction to Webhook Relay | WebhookRelay meta: "og: title": "Introduction to Webhook Relay" description: Reverse tunnels for testing and development environments url: https://webhookrelay.com/blog/introduction.md file: /blog/introduction.md --- ![Stripes](https://webhookrelay.com/blog/introduction/images/stripes.svg) # **Introduction to Webhook Relay** Reverse tunnels for testing and development environments Webhook Relay was created to solve one problem - simplify communication between systems by providing a webhook delivery system to applications that are running in internal networks. While I was working on multiple different projects I noticed that if you have an internal system that depends on a [Github](https://github.com/) (or any other public SCM) repository - you either have to poll or somehow notify your system when to poll since using webhooks is not an option. While polling is an option in some cases - it can be quite limiting since you have to wait for several minutes for your changes to be picked up by internal CI or other system. So the feedback loop increases and productivity drops. There is just no excuse for this waiting time as nothing is happening and people get bored. Polling simply does not scale. And everyone wants to scale in 2017. Imagine a system that has to check tens or hundreds of repositories every minute - you would almost instantly reach the rate limits and start looking for other solutions. ## [How does Webhook Relay work?](#how-does-webhook-relay-work) First of all, [register](https://my.webhookrelay.com/register) and get access token [here](https://my.webhookrelay.com/tokens). Basic 3 steps are required: 1. In the **buckets** page, **inputs** and **outputs** are defined. Create at least one input and one output (where webhook should be relayed, i.e. `https://127.0.0.1:8000/github-webhook/`). 2. In your repository go to settings page and then webhooks ([https://github.com/\[user\]/\[repo\]/settings/hooks](https://github.com/%5Buser%5D/%5Brepo%5D/settings/hooks)) and add the address from previous step. It looks something like this: `https://my.webhookrelay.com/v1/webhooks/61798116-42ac-4fa4-b731-a3d488a4617a` 3. Get the latest [relay client](https://docs.webhookrelay.com/installation-options/installation-options/install-cli#download-and-install)and start it in internal network (i.e. your laptop). It opens a reverse tunnel and maintains a long lived connection. Agent should also be able to reach [https://127.0.0.1:8000/github-webhook/](https://127.0.0.1:8000/github-webhook/). Since agent is establishing connection from inside there is no need to configure firewalls or open ports. That's it. Based on a user supplied token the agent will be mapped to a corresponding user and it will start receiving incoming webhooks with desired destinations so it can start forwarding. So the workflow looks like: ![Webhooks](https://webhookrelay.com/blog/introduction/images/webhookrelay_basic_diagram.png) I hope this article was useful. New articles incoming, stay tuned! P.S. Login/Registration [here](https://my.webhookrelay.com/login) --- --- title: TLS Compatibility: Custom & Legacy TLS Versions meta: "og: title": "TLS Compatibility: Custom & Legacy TLS Versions" description: Receive webhooks from legacy systems that can't speak modern TLS. Set a minimum TLS version per input, accept TLS 1.0/1.1, and skip verification per output. url: https://webhookrelay.com/features/tls-compatibility.md file: /features/tls-compatibility.md --- ![Stripes](https://webhookrelay.com/features/tls-compatibility/images/stripes.svg) **Plans: Business** FEATURES # **TLS Compatibility: Custom & Legacy TLS Versions** Receive webhooks from legacy systems that can't speak modern TLS. Set a minimum TLS version per input, accept TLS 1.0/1.1, and skip verification per output. **Some systems are stuck in the past — your security baseline shouldn't be.** A legacy device, an old ERP, or an on-prem appliance that can only speak TLS 1.0/1.1 (or an outdated cipher) simply _can't deliver_ a webhook to a modern endpoint that has, correctly, disabled those protocols. Webhook Relay's **TLS compatibility** lets that one old sender through — on a single input — while everything else stays on modern TLS. ## [The problem: a legacy sender meets a modern endpoint](#the-problem-a-legacy-sender-meets-a-modern-endpoint) When an old system tries to POST a webhook over a TLS version or cipher the receiver has disabled, neither side will budge, and the delivery dies with one of these: ``` curl: (35) error:0A000102:SSL routines::unsupported protocol error:14094410:SSL routines:ssl3_read_bytes:sslv3 alert handshake failure javax.net.ssl.SSLHandshakeException: Received fatal alert: protocol_version ERR_SSL_VERSION_OR_CIPHER_MISMATCH ``` The usual "fix" is to lower TLS everywhere the old sender might reach — weakening your whole stack for one stubborn box. (For a full reference of these errors and what each means, see the [TLS/SSL error guide](https://webhookrelay.com/features/tls-compatibility/docs/webhooks/tls-ssl-errors/).) ## [The solution: relax TLS on one input, not everywhere](#the-solution-relax-tls-on-one-input-not-everywhere) Give the legacy sender a Webhook Relay **input** and set the TLS policy on that input alone. Each input has a **TLS compatibility** setting with two controls: - **Custom minimum TLS version** — the lowest version the input will accept. The default is TLS 1.3; lower it to TLS 1.2 for senders that still require it (PayPal webhooks, for example). _(Business and Pro)_ - **Legacy TLS compatibility (TLS 1.0 + wide ciphers)** — one toggle that makes the input accept TLS versions down to 1.0 and a wider legacy cipher set, for the oldest systems that can't do anything newer. _(Pro)_ ![Per-input TLS compatibility settings — a minimum TLS version dropdown and a "Legacy TLS compatibility (TLS 1.0 + wide ciphers)" toggle](https://webhookrelay.com/features/tls-compatibility/images/docs/webhooks/tls/tls_settings.png) Webhook Relay completes the old handshake on that input, then forwards the event onward over modern TLS. The legacy sender finally gets through, and the rest of your traffic never leaves TLS 1.2/1.3. ## [Deliver to self-signed and internal certificates](#deliver-to-self-signed-and-internal-certificates) The other half of the gap is on the delivery side. When you forward to an internal service or legacy box whose certificate isn't signed by a public CA, strict clients fail with `certificate verify failed` or `unable to verify the first certificate`. Webhook Relay exposes a per-destination **TLS verification** toggle under **Delivery controls** — switch it off for that one endpoint and the webhook is delivered. ![Per-output Delivery controls with a TLS verification toggle](https://webhookrelay.com/features/tls-compatibility/images/docs/webhooks/tls/tls_output_disable_verification.png) Only disable verification for destinations you control and trust, typically on a private network. ## [Why a relay is the right place to solve it](#why-a-relay-is-the-right-place-to-solve-it) - **One input, not a fleet of clients.** Relax TLS on the single input a legacy sender uses, instead of lowering the security baseline everywhere it might connect. - **Contain the risk.** Legacy settings apply **per input domain**, so the rest of your inputs keep enforcing modern TLS 1.3. - **Enforce a minimum, too.** Pin a custom minimum TLS version on an input to satisfy a compliance requirement and reject anything weaker. - **Reach internal endpoints.** Deliver onward to services behind your firewall through the [Webhook Relay agent](https://webhookrelay.com/features/tls-compatibility/features/webhook-to-internal-server/) — self-signed certificate and all. ## [Where it earns its keep](#where-it-earns-its-keep) - **Legacy on-prem and IoT senders.** Appliances, controllers and old ERPs that will never get a TLS upgrade but still need to emit events. - **Integrations pinned to TLS 1.2.** Senders like PayPal that require a specific older version to connect. - **Compliance-driven minimums.** Enforce a floor on accepted TLS versions without auditing every producer. - **Internal and self-signed destinations.** Forward to a service whose certificate a public CA would never vouch for. ## [Better together](#better-together) TLS compatibility pairs naturally with [durable retries](https://webhookrelay.com/features/tls-compatibility/features/durable-retries/) — persist and retry until the event lands — and [throttling](https://webhookrelay.com/features/tls-compatibility/features/throttling/), so a fragile legacy system is never overwhelmed. Ready to let your legacy systems through? [Create a free account](https://my.webhookrelay.com/register), or read the [TLS/SSL error reference](https://webhookrelay.com/features/tls-compatibility/docs/webhooks/tls-ssl-errors/) to match the exact error you're seeing. ![Stripes](https://webhookrelay.com/features/tls-compatibility/images/stripes-dark.svg) ## **Start forwarding webhooks in minutes ** Connect a source, pick a destination, and Webhook Relay handles delivery, retries and transforms. Set up your first webhook in under five minutes. [Start for free ->](https://my.webhookrelay.com/register) Free plan · No credit card required · 7-day money-back guarantee on paid plans --- --- title: Changelog | WebhookRelay meta: "og: title": Changelog description: Release notes and version history for the Webhook Relay server, agent and CLI — track new features, fixes and improvements across every release. url: https://webhookrelay.com/changelog.md file: /changelog.md --- # **Changelog** Release notes and version history for the Webhook Relay server, agent and CLI — track new features, fixes and improvements across every release. ## [Server Releases](#server-releases) ### [Latest Release:](#latest-release) **0.179.20** - Released on 2026-05-23 - chore(logging): cut non-actionable log noise pinning GCP Log Storage (PR: [#793](https://github.com/rusenask/webhookrelay/pull/793)) **0.179.19** - Released on 2026-05-22 - fix(grpc): skip UpdateWebhook on Preparing status (CH write amplification fix) (PR: [#792](https://github.com/rusenask/webhookrelay/pull/792)) **0.179.18** - Released on 2026-05-22 - fix(turbostore): keep wide-range reads bounded (PR: [#791](https://github.com/rusenask/webhookrelay/pull/791)) **0.179.17** - Released on 2026-05-22 - fix(split-recorder): ClickHouse saturation mitigation — pool cap + negative cache + extended TTL (PR: [#790](https://github.com/rusenask/webhookrelay/pull/790)) **0.179.16** - Released on 2026-05-22 - fix(split-recorder): read-through repopulation for GetLog cache (PR: [#789](https://github.com/rusenask/webhookrelay/pull/789)) ### [Misc Fixes/Maintenance](#misc-fixesmaintenance) - 0.179.15 & below: Minor fixes and maintenance updates. ## [Client Releases](#client-releases) ### [Latest Release:](#latest-release-1) **1.34.4** - Released on 2025-08-08 - fix forward without outputs (PR: [#121](https://github.com/rusenask/client/pull/121)) **1.34.3** - Released on 2025-06-26 - Feature/cdn download (PR: [#119](https://github.com/rusenask/client/pull/119)) - retries (PR: [#120](https://github.com/rusenask/client/pull/120)) **1.34.2** - Released on 2025-05-03 - Feature/tunnel cli fixes (PR: [#117](https://github.com/rusenask/client/pull/117)) - cleanup (PR: [#118](https://github.com/rusenask/client/pull/118)) **1.34.1** - Released on 2025-04-27 - Feature/tty fallback (PR: [#116](https://github.com/rusenask/client/pull/116)) **1.34.0** - Released on 2025-04-25 - Feature/connect tui (PR: [#115](https://github.com/rusenask/client/pull/115)) ---