Skip to content

Custom Webhooks

Integrate any webhook provider using generic HMAC verification or custom headers.

When to Use Custom Integration

Use custom webhooks when:

  • Your provider isn't specifically supported
  • You're building your own webhook sender
  • You need custom signature verification
  • You want to receive webhooks without verification (development only)

Generic HMAC Verification

Most webhook providers use HMAC signatures. Configure generic HMAC verification:

bash
curl -X POST https://api.webhookrelay.com/api/organizations/{orgId}/sources \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Custom Webhooks",
    "slug": "custom",
    "verificationConfig": {
      "type": "hmac",
      "secret": "your-webhook-secret",
      "algorithm": "sha256",
      "header": "X-Signature",
      "encoding": "hex",
      "prefix": "sha256="
    }
  }'

Configuration Options

OptionDescriptionValues
algorithmHash algorithmsha1, sha256, sha512
headerHeader containing signatureAny header name
encodingSignature encodinghex, base64
prefixPrefix to strip from signaturee.g., sha256=, v1=

Common Provider Configurations

Shopify

json
{
  "verificationConfig": {
    "type": "hmac",
    "secret": "your-shopify-secret",
    "algorithm": "sha256",
    "header": "X-Shopify-Hmac-SHA256",
    "encoding": "base64"
  }
}

Twilio

json
{
  "verificationConfig": {
    "type": "hmac",
    "secret": "your-auth-token",
    "algorithm": "sha1",
    "header": "X-Twilio-Signature",
    "encoding": "base64"
  }
}

SendGrid

json
{
  "verificationConfig": {
    "type": "hmac",
    "secret": "your-verification-key",
    "algorithm": "sha256",
    "header": "X-Twilio-Email-Event-Webhook-Signature",
    "encoding": "base64"
  }
}

Linear

json
{
  "verificationConfig": {
    "type": "hmac",
    "secret": "your-signing-secret",
    "algorithm": "sha256",
    "header": "Linear-Signature",
    "encoding": "hex"
  }
}

Paddle

json
{
  "verificationConfig": {
    "type": "hmac",
    "secret": "your-webhook-secret",
    "algorithm": "sha256",
    "header": "Paddle-Signature",
    "encoding": "hex",
    "prefix": "h1="
  }
}

No Verification

For development or trusted internal services:

json
{
  "verificationConfig": {
    "type": "none"
  }
}

DANGER

Never use type: none in production! This allows anyone to send webhooks to your endpoint.

API Key Authentication

Some providers authenticate via headers. Add custom headers to your destinations:

json
{
  "name": "Internal Service",
  "url": "https://internal.example.com/webhook",
  "headers": {
    "X-API-Key": "your-api-key",
    "Authorization": "Bearer your-token"
  }
}

Building Your Own Webhook Sender

Signing Webhooks

When building a system that sends webhooks, implement HMAC signing:

javascript
const crypto = require('crypto');

function signPayload(payload, secret) {
  const hmac = crypto.createHmac('sha256', secret);
  hmac.update(JSON.stringify(payload));
  return 'sha256=' + hmac.digest('hex');
}

// Send webhook
const payload = { event: 'user.created', data: { id: '123' } };
const signature = signPayload(payload, 'your-secret');

fetch('https://api.webhookrelay.com/ingest/myorg/custom', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Signature': signature
  },
  body: JSON.stringify(payload)
});

Python Example

python
import hmac
import hashlib
import json
import requests

def sign_payload(payload, secret):
    message = json.dumps(payload).encode()
    signature = hmac.new(
        secret.encode(),
        message,
        hashlib.sha256
    ).hexdigest()
    return f'sha256={signature}'

payload = {'event': 'user.created', 'data': {'id': '123'}}
signature = sign_payload(payload, 'your-secret')

requests.post(
    'https://api.webhookrelay.com/ingest/myorg/custom',
    json=payload,
    headers={
        'X-Signature': signature
    }
)

Transform Examples

Generic Event Normalizer

javascript
function transform(payload) {
  // Normalize different webhook formats to a standard structure
  return {
    type: payload.event || payload.type || payload.action || 'unknown',
    data: payload.data || payload.payload || payload,
    timestamp: payload.timestamp || payload.created_at || new Date().toISOString(),
    source: 'custom'
  };
}

Extract Common Fields

javascript
function transform(payload) {
  // Extract user info from various formats
  const user = payload.user || payload.data?.user || payload.customer || {};

  return {
    event: payload.event,
    userId: user.id || user.user_id || user.uid,
    userEmail: user.email || user.email_address,
    userName: user.name || user.display_name || user.username,
    metadata: payload.metadata || {},
    receivedAt: new Date().toISOString()
  };
}

Webhook to Slack

javascript
function transform(payload) {
  return {
    blocks: [
      {
        type: "section",
        text: {
          type: "mrkdwn",
          text: `*Webhook Received*\n\`\`\`${JSON.stringify(payload, null, 2)}\`\`\``
        }
      }
    ]
  };
}

Filter Examples

By Event Type

json
{
  "name": "User Events",
  "logic": "and",
  "conditions": [
    {
      "field": "event",
      "operator": "starts_with",
      "value": "user."
    }
  ]
}

By Environment

json
{
  "name": "Production Only",
  "logic": "and",
  "conditions": [
    {
      "field": "environment",
      "operator": "equals",
      "value": "production"
    }
  ]
}

Has Required Fields

json
{
  "name": "Valid Payload",
  "logic": "and",
  "conditions": [
    {
      "field": "event",
      "operator": "exists",
      "value": ""
    },
    {
      "field": "data.id",
      "operator": "exists",
      "value": ""
    }
  ]
}

Testing Custom Webhooks

Using cURL

bash
# Without signature (for testing with verification disabled)
curl -X POST https://api.webhookrelay.com/ingest/myorg/custom \
  -H "Content-Type: application/json" \
  -d '{"event": "test", "data": {"message": "Hello!"}}'

# With HMAC signature
PAYLOAD='{"event": "test", "data": {"message": "Hello!"}}'
SECRET="your-secret"
SIGNATURE=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print "sha256="$2}')

curl -X POST https://api.webhookrelay.com/ingest/myorg/custom \
  -H "Content-Type: application/json" \
  -H "X-Signature: $SIGNATURE" \
  -d "$PAYLOAD"

Using the Dashboard

  1. Navigate to Testing
  2. Select your custom source
  3. Enter a test payload
  4. Click Send Test

Troubleshooting

Signature Verification Failed

  1. Verify the algorithm matches your provider's implementation
  2. Check the header name is correct (case-sensitive)
  3. Ensure encoding matches (hex vs base64)
  4. Verify the prefix is stripped correctly
  5. Check if the payload is being stringified consistently

Wrong Signature Format

Common issues:

  • Using base64 when provider uses hex (or vice versa)
  • Missing or wrong prefix
  • Wrong hash algorithm
  • Extra whitespace in the signature

Payload Not Matching

Ensure you're signing the exact same bytes that are sent:

  • Same JSON serialization (key order, spacing)
  • Same character encoding (UTF-8)
  • No modifications by proxies or middleware

Released under the MIT License.