Webhooks

Receive real-time notifications when message events occur.

Overview

Webhooks let you subscribe to events like message delivery, opens, clicks, bounces, and more. When an event occurs, YourSend sends an HTTP POST request to your configured endpoint with a signed JSON payload.

Setup

You can create webhooks from the dashboard or via the API.

From the Dashboard

  1. Go to Dashboard → Webhooks
  2. Click Add Endpoint
  3. Enter your HTTPS URL (e.g., https://yourapp.com/api/webhooks/yoursend)
  4. Select the events you want to subscribe to
  5. Copy and store the signing secret — it is only shown once

Via the API

cURL
curl -X POST https://api.yoursend.dev/v1/webhooks \
  -H "Authorization: Bearer ys_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yourapp.com/api/webhooks/yoursend",
    "events": ["message.sent", "message.delivered", "message.failed"]
  }'

Event Types

Not all events fire for every channel. Open and click tracking is available for email only.

EventDescriptionChannels
message.sentMessage accepted and sent to providerEmail, SMS, Voice
message.deliveredMessage confirmed delivered to recipientEmail, SMS, Voice
message.openedRecipient opened the emailEmail
message.clickedRecipient clicked a link in the emailEmail
message.bouncedEmail bounced (hard or soft)Email
message.failedMessage delivery failedEmail, SMS, Voice
otp.verifiedOTP code was successfully verifiedEmail, SMS, Voice
otp.expiredOTP expired before verificationEmail, SMS, Voice
contact.suppressedContact auto-suppressed (bounce/complaint)Email
blast.completedBulk email blast finished sendingEmail
email.receivedInbound email received on your domainEmail

Payload Format

Every webhook delivers a JSON body with this structure:

POST https://yourapp.com/api/webhooks/yoursend
{
  "event": "message.delivered",
  "data": {
    "message_id": "550e8400-e29b-41d4-a716-446655440000",
    "to_address": "user@example.com",
    "channel": "email",
    "status": "delivered"
  },
  "timestamp": "2026-03-18T12:00:02.000Z",
  "webhook_id": "d290f1ee-6c54-4b01-90e6-d701748f0851"
}
FieldDescription
eventThe event type (e.g. message.delivered)
dataEvent-specific payload. Always includes message_id and channel for message events.
timestampISO 8601 timestamp of when the event occurred
webhook_idThe ID of the webhook endpoint that received this delivery

Request Headers

Every webhook request includes these headers:

HeaderDescription
X-YourSend-SignatureHMAC-SHA256 hex digest for verifying payload authenticity
X-YourSend-TimestampISO 8601 timestamp used in signature computation
X-YourSend-EventThe event type (same as event in body)
Content-Typeapplication/json

Signature Verification

Always verify webhook signatures to ensure the request came from YourSend. The signature is computed as HMAC-SHA256(secret, timestamp + "." + body) where timestamp is from the X-YourSend-Timestamp header and body is the raw request body.

Node.js / Express
import crypto from 'crypto';

function verifyWebhook(req: {
  body: string;
  headers: Record<string, string>;
}, secret: string): boolean {
  const signature = req.headers['x-yoursend-signature'];
  const timestamp = req.headers['x-yoursend-timestamp'];
  if (!signature || !timestamp) return false;

  // Reject requests older than 5 minutes to prevent replay attacks
  const age = Date.now() - new Date(timestamp).getTime();
  if (age > 5 * 60 * 1000) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(timestamp + '.' + req.body)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected),
  );
}
Python / Flask
import hmac, hashlib
from datetime import datetime, timezone, timedelta

def verify_webhook(body: bytes, headers: dict, secret: str) -> bool:
    signature = headers.get("X-YourSend-Signature", "")
    timestamp = headers.get("X-YourSend-Timestamp", "")
    if not signature or not timestamp:
        return False

    # Reject requests older than 5 minutes
    event_time = datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
    if datetime.now(timezone.utc) - event_time > timedelta(minutes=5):
        return False

    expected = hmac.new(
        secret.encode(),
        (timestamp + "." + body.decode()).encode(),
        hashlib.sha256,
    ).hexdigest()

    return hmac.compare_digest(signature, expected)

Important: Always use a timing-safe comparison (e.g. timingSafeEqual or hmac.compare_digest) to prevent timing attacks. Use the raw request body string, not a re-serialized object.

Retries

If your endpoint returns a non-2xx status code or times out (10 second limit), YourSend makes the initial attempt plus 2 retries on a fixed schedule:

AttemptDelay
1st retry~1 minute
2nd retry~5 minutes

After the initial attempt and 2 retries fail, the delivery is dropped. Your endpoint should return a 200status code as quickly as possible — process the payload asynchronously if needed.