Webhooks Reference
Relay uses webhooks for two purposes:
- Incoming Webhooks: Receive events from third-party services (email providers, messaging platforms, payment processors)
- Outgoing Webhooks: Send events from Relay to your external systems
This document covers both incoming webhook endpoints and outgoing webhook configuration.
Incoming Webhooks Overview
Relay provides REST webhook endpoints to receive events from integrated services. These endpoints handle signature verification, event parsing, and ticket/conversation creation.
| Service | Endpoint | Method |
|---|---|---|
| Email (Mailgun/SendGrid/Postmark) | /api/webhooks/inbound-email | POST |
| Twilio SMS | /api/webhooks/sms | POST |
| WhatsApp Business | /api/webhooks/whatsapp | GET, POST |
| Facebook Messenger | /api/webhooks/messenger | GET, POST |
| Dialpad | /api/webhooks/dialpad | POST |
| Shopify | /api/webhooks/shopify | POST |
| Shopify GDPR | /api/webhooks/shopify/gdpr | POST |
| Stripe | /api/webhooks/stripe | POST |
| Stripe Connect | /api/webhooks/stripe-connect | POST |
| Slack | /api/webhooks/slack | POST |
| Resend | /api/webhooks/resend | POST |
| Resend (legacy alias) | /api/webhooks/email | POST |
| Amazon Seller Central (SNS) | /api/amazon/webhook | POST |
Email Webhooks
Endpoint
POST /api/webhooks/inbound-email
Supported Providers
- Mailgun - Inbound email routing
- SendGrid - Inbound Parse webhook
- Postmark - Inbound webhook
- Cloudmailin - Email to HTTP
Mailgun Configuration
- Go to Mailgun Dashboard → Receiving → Create Route
- Set the route expression to match your support email domain
- Set the action to forward to your webhook URL:
https://your-domain.com/api/webhooks/inbound-email
Signature Verification:
Mailgun includes signature data in the form payload:
timestamp- Unix timestamptoken- Random stringsignature- HMAC SHA256 of timestamp + token
# Verification formula
signature = HMAC-SHA256(api_key, timestamp + token)
Payload Format (multipart/form-data):
| Field | Description |
|---|---|
from | Sender email address |
sender | Envelope sender |
recipient | Recipient email |
subject | Email subject |
body-plain | Plain text body |
body-html | HTML body |
stripped-text | Body without quoted text |
stripped-html | HTML without quoted text |
message-headers | JSON array of headers |
In-Reply-To | Reply threading header |
References | Message reference chain |
attachment-count | Number of attachments |
attachment-{n} | File attachments |
SendGrid Configuration
- Go to SendGrid → Settings → Inbound Parse
- Add your domain and set the destination URL:
https://your-domain.com/api/webhooks/inbound-email
Signature Verification:
SendGrid uses two headers:
x-twilio-email-event-webhook-signature- ECDSA signaturex-twilio-email-event-webhook-timestamp- Unix timestamp
Payload Format (multipart/form-data):
| Field | Description |
|---|---|
from | Sender email |
to | Recipient email |
subject | Email subject |
text | Plain text body |
html | HTML body |
headers | Full email headers |
envelope | JSON with sender/recipients |
attachments | Number of attachments |
attachment{n} | File attachments |
Postmark Configuration
- Go to Postmark → Servers → Inbound
- Set the webhook URL:
https://your-domain.com/api/webhooks/inbound-email
Signature Verification:
Include your webhook token in the URL or x-postmark-token header.
Payload Format (application/json):
{
"From": "sender@example.com",
"FromName": "John Doe",
"To": "support@company.com",
"Subject": "Help needed",
"TextBody": "Plain text content",
"HtmlBody": "<p>HTML content</p>",
"ReplyTo": "sender@example.com",
"MessageID": "unique-id",
"Headers": [
{"Name": "In-Reply-To", "Value": "<message-id>"}
],
"Attachments": [
{
"Name": "file.pdf",
"ContentType": "application/pdf",
"Content": "base64-encoded-content"
}
]
}
Email Threading
Relay automatically threads emails to existing tickets using:
- In-Reply-To header - Direct reply reference
- References header - Full message chain
- Subject pattern -
[Ticket #123]in subject line - Message-ID tracking - Stored for each conversation
Email Classification
Incoming emails are classified as:
| Type | Description |
|---|---|
inbound | New customer message |
bounce | Delivery failure notification |
outbound_copy | BCC copy of sent email |
auto_reply | Automatic reply (vacation, etc.) |
dialpad_voicemail | Voicemail notification from Dialpad |
Auto-replies and bounces are handled specially to avoid creating unnecessary tickets.
SMS Webhooks (Twilio)
Endpoint
POST /api/webhooks/sms
Configuration
- Go to Twilio Console → Phone Numbers → Manage → Active Numbers
- Select your number and set the webhook URL for incoming messages:
https://your-domain.com/api/webhooks/sms - Set the HTTP method to POST
Payload Format (application/x-www-form-urlencoded):
| Field | Description |
|---|---|
From | Sender phone number (E.164 format) |
To | Your Twilio number |
Body | Message content |
MessageSid | Unique message identifier |
NumMedia | Number of media attachments |
MediaUrl0, MediaUrl1, ... | URLs to media files |
MediaContentType0, ... | MIME types of media |
Response Format:
The endpoint returns TwiML XML. For auto-replies:
<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Message>Thank you for your message. We'll respond shortly.</Message>
</Response>
For no response:
<?xml version="1.0" encoding="UTF-8"?>
<Response></Response>
SMS Threading
SMS conversations are threaded by phone number. All messages from the same number are grouped into a single ticket thread.
WhatsApp Webhooks
Endpoint
GET /api/webhooks/whatsapp- Webhook verificationPOST /api/webhooks/whatsapp- Receive messages
Configuration
- Go to Meta Developer Portal → Your App → WhatsApp → Configuration
- Set the webhook URL:
https://your-domain.com/api/webhooks/whatsapp - Set a verify token (store this in your integration settings)
- Subscribe to:
messages
Webhook Verification
Meta verifies your webhook with a GET request:
Query Parameters:
| Parameter | Description |
|---|---|
hub.mode | Always subscribe |
hub.verify_token | Your configured token |
hub.challenge | Challenge string to echo back |
Response: Return the hub.challenge value with status 200.
Message Webhook
Headers:
| Header | Description |
|---|---|
x-hub-signature-256 | HMAC SHA256 signature |
Signature Verification:
signature = HMAC-SHA256(app_secret, raw_body)
# Compare with x-hub-signature-256 header (format: sha256=...)
Payload Format:
{
"object": "whatsapp_business_account",
"entry": [
{
"id": "WHATSAPP_BUSINESS_ACCOUNT_ID",
"changes": [
{
"value": {
"messaging_product": "whatsapp",
"metadata": {
"display_phone_number": "15551234567",
"phone_number_id": "PHONE_NUMBER_ID"
},
"contacts": [
{
"profile": {"name": "Customer Name"},
"wa_id": "15559876543"
}
],
"messages": [
{
"from": "15559876543",
"id": "wamid.xxx",
"timestamp": "1234567890",
"type": "text",
"text": {"body": "Hello, I need help"}
}
]
},
"field": "messages"
}
]
}
]
}
Message Types
| Type | Description |
|---|---|
text | Plain text message |
image | Image with optional caption |
document | File attachment |
audio | Voice message |
video | Video message |
location | Location share |
contacts | Contact card |
interactive | Button/list response |
Facebook Messenger Webhooks
Endpoint
GET /api/webhooks/messenger- Webhook verificationPOST /api/webhooks/messenger- Receive messages
Configuration
- Go to Meta Developer Portal → Your App → Messenger → Settings
- Set the webhook URL:
https://your-domain.com/api/webhooks/messenger - Set a verify token
- Subscribe to:
messages,messaging_postbacks
Webhook Verification
Same as WhatsApp - return hub.challenge when hub.verify_token matches.
Message Webhook
Headers:
| Header | Description |
|---|---|
x-hub-signature-256 | HMAC SHA256 signature |
Payload Format:
{
"object": "page",
"entry": [
{
"id": "PAGE_ID",
"time": 1234567890,
"messaging": [
{
"sender": {"id": "PSID"},
"recipient": {"id": "PAGE_ID"},
"timestamp": 1234567890,
"message": {
"mid": "MESSAGE_ID",
"text": "Hello, I need help",
"attachments": [
{
"type": "image",
"payload": {"url": "https://..."}
}
]
}
}
]
}
]
}
Event Types
| Event | Description |
|---|---|
message | User sent a message |
postback | User clicked a button (e.g., Get Started) |
read | User read a message |
delivery | Message was delivered |
Dialpad Webhooks
Endpoint
POST /api/webhooks/dialpad
Configuration
- In Dialpad Admin → Company Settings → Integrations
- Or via Dialpad API, register webhook URL:
https://your-domain.com/api/webhooks/dialpad
Signature Verification
Header: x-dialpad-signature
signature = HMAC-SHA256(webhook_secret, raw_body)
Call Events
Payload Format:
{
"call_id": "unique-call-id",
"state": "connected",
"direction": "inbound",
"target": {
"phone_number": "+15551234567",
"name": "Support Line"
},
"contact": {
"phone_number": "+15559876543",
"name": "Customer Name"
},
"started_at": 1234567890,
"answered_at": 1234567891,
"duration": 120
}
Call States
| State | Description |
|---|---|
calling | Outbound call initiated |
ringing | Call ringing |
connected | Call answered |
hold | Call on hold |
hangup | Call ended |
missed | Call not answered |
voicemail | Went to voicemail |
voicemail_uploaded | Voicemail recording ready |
transcription | Transcription available |
recording | Call recording ready |
SMS Events
{
"event_type": "sms",
"direction": "inbound",
"from_number": "+15559876543",
"to_number": "+15551234567",
"text": "Message content",
"timestamp": 1234567890
}
Voicemail Processing
When a voicemail is received:
voicemailstate event firesvoicemail_uploadedfires when audio is readytranscriptionfires when text transcription is ready- Relay creates a ticket with the voicemail audio and transcription
Shopify Webhooks
Endpoint
POST /api/webhooks/shopify
Configuration
Webhooks are automatically registered when you connect your Shopify store. Relay subscribes to:
orders/createorders/updatedcustomers/createcustomers/update
Signature Verification
Header: x-shopify-hmac-sha256
signature = Base64(HMAC-SHA256(webhook_secret, raw_body))
Payload Format
Shopify sends standard webhook payloads. See Shopify Webhook Documentation.
Shopify GDPR Webhooks
Endpoint
POST /api/webhooks/shopify/gdpr
Topics
Shopify requires the following GDPR topics:
customers/data_requestcustomers/redactshop/redact
Signature Verification
Header: x-shopify-hmac-sha256
Uses the Shopify API secret to validate HMAC signatures.
Amazon SNS Webhooks
Endpoint
POST /api/amazon/webhook
Purpose
Receives Amazon Seller Central notifications via SNS and processes order and return events.
Notes
- Supports SNS subscription confirmation
- Verifies SNS signing certificate host before processing
Stripe Webhooks
Endpoint
POST /api/webhooks/stripe
Configuration
- Go to Stripe Dashboard → Developers → Webhooks
- Add endpoint:
https://your-domain.com/api/webhooks/stripe - Select events to subscribe to
Signature Verification
Header: stripe-signature
Uses Stripe's official signature verification. See Stripe Webhook Signatures.
Subscribed Events
checkout.session.completedcustomer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deletedinvoice.payment_succeededinvoice.payment_failed
Outgoing Webhooks
Configure outgoing webhooks to receive events from Relay in your external systems.
Configuration
Procedure: webhooks.create
{
url: string; // Your webhook endpoint URL
events: string[]; // Events to subscribe to
secret?: string; // Optional signing secret
enabled?: boolean; // default: true
}
Available Events
| Event | Description |
|---|---|
ticket.created | New ticket created |
ticket.updated | Ticket fields changed |
ticket.status_changed | Status changed |
ticket.assigned | Ticket assigned |
ticket.priority_changed | Priority changed |
conversation.created | New message/reply added |
customer.created | New customer created |
customer.updated | Customer updated |
Payload Format
All outgoing webhooks use a consistent format:
{
"event": "ticket.created",
"timestamp": "2024-01-15T10:30:00.000Z",
"data": {
"ticketId": "uuid",
"ticketNumber": 1234,
"subject": "Need help with...",
"status": "open",
"priority": "normal",
"customerId": "uuid",
"customerEmail": "customer@example.com"
}
}
Signature Verification
If you provide a secret when creating the webhook, Relay signs payloads:
Header: x-relay-signature
signature = HMAC-SHA256(secret, raw_body)
Retry Policy
Failed webhook deliveries are retried:
- Retry 1: After 1 minute
- Retry 2: After 5 minutes
- Retry 3: After 30 minutes
- Retry 4: After 2 hours
- Retry 5: After 24 hours
After 5 failed attempts, the webhook is marked as failed and disabled.
Managing Webhooks
List Webhooks:
curl "https://your-domain.com/api/trpc/webhooks.list" \
-H "Cookie: your-session-cookie"
Create Webhook:
curl -X POST "https://your-domain.com/api/trpc/webhooks.create" \
-H "Content-Type: application/json" \
-H "Cookie: your-session-cookie" \
-d '{
"json": {
"url": "https://your-server.com/webhook",
"events": ["ticket.created", "ticket.status_changed"],
"secret": "your-signing-secret"
}
}'
Update Webhook:
curl -X POST "https://your-domain.com/api/trpc/webhooks.update" \
-H "Content-Type: application/json" \
-H "Cookie: your-session-cookie" \
-d '{
"json": {
"id": "webhook-uuid",
"events": ["ticket.created", "conversation.created"],
"enabled": true
}
}'
Delete Webhook:
curl -X POST "https://your-domain.com/api/trpc/webhooks.delete" \
-H "Content-Type: application/json" \
-H "Cookie: your-session-cookie" \
-d '{"json":{"id":"webhook-uuid"}}'
Test Webhook:
curl -X POST "https://your-domain.com/api/trpc/webhooks.test" \
-H "Content-Type: application/json" \
-H "Cookie: your-session-cookie" \
-d '{"json":{"id":"webhook-uuid"}}'
Best Practices
For Incoming Webhooks
- Always verify signatures - Never process unverified webhooks
- Return 200 quickly - Process events asynchronously to avoid timeouts
- Handle duplicates - Webhooks may be retried; use idempotency keys
- Log webhook payloads - Helps debug integration issues
For Outgoing Webhooks
- Use HTTPS endpoints - Webhook data may contain sensitive information
- Verify signatures - Validate the
x-relay-signatureheader - Handle retries - Your endpoint should be idempotent
- Respond quickly - Return 2xx within 30 seconds
Troubleshooting
Webhook not receiving events
- Check that the webhook URL is publicly accessible
- Verify the webhook is enabled in your integration settings
- Check your server logs for incoming requests
- Ensure your firewall allows requests from the service's IP ranges
Signature verification failing
- Ensure you're using the raw request body (not parsed JSON)
- Check that your secret matches the configured value
- Verify the signature algorithm matches (SHA256 vs SHA1)
Missing events
- Check that you've subscribed to the correct event types
- Verify the integration is properly connected
- Check the webhook delivery logs in Relay dashboard