Moorsyl Docs

Webhooks

Receive signed real-time event notifications when messages are delivered or verifications complete.

Webhooks let Moorsyl push events to your server the moment something happens — a message delivered, a verification approved. Instead of polling the API, you register an HTTPS endpoint and Moorsyl calls it for you.

Supported events

EventTriggered when
sms.sentAn SMS was successfully delivered to the carrier
sms.failedAn SMS permanently failed after all retries
verify.sentA verification code was sent to a phone number
verify.approvedA user entered the correct verification code
verify.failedA verification session expired or max attempts were exceeded

Set up an endpoint

  1. Open the dashboard → Webhooks
  2. Click Add Endpoint
  3. Enter your HTTPS URL — local URLs (localhost) are not accepted
  4. Select the events you want to subscribe to
  5. Copy the signing secret (whsec_…) — it is shown only once

Moorsyl sends a POST request to your endpoint for each subscribed event.

Event payload

All events share the same envelope:

{
  "type": "verify.approved",
  "verificationId": "ver_...",
  "to": "+22236551999",
  "organizationId": "org_..."
}

sms.sent

{
  "type": "sms.sent",
  "smsId": "sms_...",
  "to": "+22236551999",
  "providerMessageId": "provider-ref-...",
  "organizationId": "org_..."
}

sms.failed

{
  "type": "sms.failed",
  "smsId": "sms_...",
  "to": "+22236551999",
  "errorText": "Carrier rejected message",
  "organizationId": "org_..."
}

verify.sent

{
  "type": "verify.sent",
  "verificationId": "ver_...",
  "to": "+22236551999",
  "organizationId": "org_..."
}

verify.approved

{
  "type": "verify.approved",
  "verificationId": "ver_...",
  "to": "+22236551999",
  "organizationId": "org_..."
}

verify.failed

{
  "type": "verify.failed",
  "verificationId": "ver_...",
  "to": "+22236551999",
  "organizationId": "org_..."
}

Verifying signatures

Every request from Moorsyl includes a Moorsyl-Signature header. You should verify this header before processing the event to ensure the request is genuine.

Header format

Moorsyl-Signature: t=1712345678,v1=a3f2b1c4d5e6...
  • t — Unix timestamp in seconds of when the event was sent
  • v1 — HMAC-SHA256 signature

Verification steps

  1. Extract t and v1 from the header
  2. Construct the signed string: "${t}.${rawRequestBody}"
  3. Compute HMAC-SHA256 of the signed string using your whsec_… secret (base64-decoded)
  4. Compare your computed signature to v1 using a constant-time comparison
Verify signature (TypeScript)
import { createHmac, timingSafeEqual } from "node:crypto";

function verifyWebhook(
  rawBody: string,
  header: string,
  secret: string // your whsec_... value
): boolean {
  const parts = Object.fromEntries(
    header.split(",").map((p) => p.split("=") as [string, string])
  );
  const timestamp = parts["t"];
  const signature = parts["v1"];

  if (!timestamp || !signature) return false;

  // Reject events older than 5 minutes to prevent replay attacks
  const age = Math.floor(Date.now() / 1000) - Number(timestamp);
  if (age > 300) return false;

  const keyBytes = Buffer.from(secret.replace("whsec_", ""), "base64");
  const expected = createHmac("sha256", keyBytes)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");

  return timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}
Verify signature (Python)
import hmac
import hashlib
import base64
import time

def verify_webhook(raw_body: str, header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    timestamp = parts.get("t")
    signature = parts.get("v1")

    if not timestamp or not signature:
        return False

    # Reject events older than 5 minutes
    if abs(time.time() - int(timestamp)) > 300:
        return False

    key = base64.b64decode(secret.removeprefix("whsec_"))
    expected = hmac.new(key, f"{timestamp}.{raw_body}".encode(), hashlib.sha256).hexdigest()

    return hmac.compare_digest(expected, signature)

Always use a constant-time comparison (timingSafeEqual / hmac.compare_digest) to avoid timing attacks.

Retry schedule

If your endpoint does not return a 2xx response within 10 seconds, Moorsyl marks the delivery as failed and retries automatically:

AttemptDelay after previous failure
1 (initial)Immediate
25 minutes
330 minutes
42 hours
58 hours

After 5 failed attempts the event is marked failed and no further retries are made. You can view failed events in the dashboard → WebhooksEvents.

Rotate a signing secret

If your signing secret is compromised:

  1. Open the dashboard → Webhooks
  2. Click the menu on your endpoint → Rotate Secret
  3. Copy and store the new whsec_… secret
  4. Update your verification logic with the new secret

The old secret stops working immediately after rotation.

Best practices

  • Always verify the Moorsyl-Signature header before trusting the payload
  • Reject events with a timestamp older than 5 minutes (replay attack protection)
  • Return a 2xx response quickly — do heavy processing asynchronously
  • Make your handler idempotent — the same event may be delivered more than once
  • Monitor the Events log in the dashboard for delivery failures

On this page