Optare v1.0 is now available. Get started →
Learn
Webhooks

Webhooks

Webhooks allow you to receive real-time notifications when events occur in your Optare SSO instance. Instead of polling for changes, Optare will send HTTP POST requests to your specified URLs.

Overview

Webhooks are useful for:

  • Synchronizing user data - Keep your CRM or database in sync
  • Triggering workflows - Start automation when users sign up
  • Audit logging - Track all authentication events
  • Analytics - Send events to your analytics platform
  • Notifications - Alert your team when important events occur

Available Events

User Events

  • user.created - New user registered
  • user.updated - User profile changed
  • user.deleted - User account deleted
  • user.email.verified - Email verification completed
  • user.password.changed - Password was changed

Session Events

  • session.created - User signed in
  • session.revoked - Session was terminated
  • session.expired - Session naturally expired

Organization Events

  • organization.created - New organization created
  • organization.updated - Organization settings changed
  • organization.deleted - Organization was deleted

Member Events

  • member.added - User added to organization
  • member.removed - User removed from organization
  • member.role.changed - Member role updated
  • member.invitation.sent - Invitation email sent
  • member.invitation.accepted - Invitation accepted
  • member.invitation.expired - Invitation expired

OAuth Events

  • oauth.client.created - OAuth client registered
  • oauth.client.deleted - OAuth client removed
  • oauth.authorization.granted - User authorized an app
  • oauth.token.issued - Access token generated

Security Events

  • 2fa.enabled - Two-factor authentication enabled
  • 2fa.disabled - Two-factor authentication disabled
  • api_key.created - API key generated
  • api_key.revoked - API key revoked

Creating a Webhook

Via Dashboard

  1. Navigate to SettingsWebhooks
  2. Click Create Webhook
  3. Enter your webhook URL
  4. Select the events you want to receive
  5. Click Create

Via API

import { db } from "~/lib/db";
import { webhooks } from "~/lib/db/schema";
import crypto from "crypto";
 
export async function createWebhook(
  organizationId: string,
  url: string,
  events: string[]
) {
  const secret = crypto.randomBytes(32).toString("hex");
 
  const [webhook] = await db.insert(webhooks).values({
    id: crypto.randomUUID(),
    organizationId,
    url,
    events,
    secret,
    isActive: true,
  }).returning();
 
  return webhook;
}

Webhook Payload

All webhooks send a POST request with a JSON payload:

{
  event: string;           // Event type (e.g., "user.created")
  timestamp: string;       // ISO 8601 timestamp
  id: string;             // Unique event ID
  data: {
    // Event-specific data
  };
  organization: {
    id: string;
    name: string;
  };
}

Example: User Created Event

{
  "event": "user.created",
  "timestamp": "2025-11-11T10:30:00Z",
  "id": "evt_1a2b3c4d",
  "data": {
    "user": {
      "id": "user_123",
      "email": "john@example.com",
      "name": "John Doe",
      "emailVerified": false,
      "createdAt": "2025-11-11T10:30:00Z"
    }
  },
  "organization": {
    "id": "org_abc",
    "name": "Acme Inc"
  }
}

Example: Member Invitation Accepted

{
  "event": "member.invitation.accepted",
  "timestamp": "2025-11-11T14:30:00Z",
  "id": "evt_5e6f7g8h",
  "data": {
    "invitation": {
      "id": "inv_xyz",
      "email": "jane@example.com",
      "role": "member",
      "invitedBy": "user_owner"
    },
    "user": {
      "id": "user_456",
      "email": "jane@example.com",
      "name": "Jane Smith"
    },
    "organization": {
      "id": "org_abc",
      "name": "Acme Inc"
    }
  }
}

Handling Webhooks

Basic Webhook Handler

// app/routes/webhooks.optare.tsx
import { json, type ActionFunctionArgs } from "@remix-run/node";
import crypto from "crypto";
 
export async function action({ request }: ActionFunctionArgs) {
  // Verify webhook signature
  const signature = request.headers.get("X-Optare-Signature");
  const body = await request.text();
  
  if (!verifySignature(body, signature, process.env.WEBHOOK_SECRET!)) {
    return json({ error: "Invalid signature" }, { status: 401 });
  }
 
  const event = JSON.parse(body);
 
  // Handle different event types
  switch (event.event) {
    case "user.created":
      await handleUserCreated(event.data);
      break;
    
    case "member.invitation.accepted":
      await handleMemberJoined(event.data);
      break;
    
    case "session.created":
      await handleUserSignIn(event.data);
      break;
    
    default:
      console.log(`Unhandled event: ${event.event}`);
  }
 
  return json({ received: true });
}
 
function verifySignature(
  payload: string,
  signature: string | null,
  secret: string
): boolean {
  if (!signature) return false;
 
  const hmac = crypto.createHmac("sha256", secret);
  const digest = hmac.update(payload).digest("hex");
  
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(digest)
  );
}
 
async function handleUserCreated(data: any) {
  // Sync user to your CRM
  await syncToCRM(data.user);
  
  // Send welcome email
  await sendWelcomeEmail(data.user.email);
  
  // Track analytics
  await trackEvent("user_signed_up", data.user.id);
}

Security

Verifying Webhook Signatures

All webhooks include an X-Optare-Signature header containing an HMAC SHA-256 signature:

import crypto from "crypto";
 
function verifyWebhook(
  payload: string,
  signature: string,
  secret: string
): boolean {
  const hmac = crypto.createHmac("sha256", secret);
  const digest = hmac.update(payload).digest("hex");
  
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(digest)
  );
}
 
// Usage in your webhook handler
export async function action({ request }: ActionFunctionArgs) {
  const signature = request.headers.get("X-Optare-Signature");
  const body = await request.text();
  const secret = process.env.WEBHOOK_SECRET!;
 
  if (!verifyWebhook(body, signature, secret)) {
    throw new Response("Invalid signature", { status: 401 });
  }
 
  // Process webhook...
}

Best Practices

  1. Always verify signatures - Never trust incoming webhooks without verification
  2. Use HTTPS endpoints - Only use HTTPS URLs for webhook endpoints
  3. Return quickly - Respond with 200 OK quickly, then process asynchronously
  4. Implement retries - Handle webhook failures gracefully
  5. Log everything - Keep audit logs of all received webhooks
  6. Use idempotency keys - Handle duplicate webhook deliveries

Retry Logic

Optare automatically retries failed webhook deliveries:

  • Attempt 1: Immediately
  • Attempt 2: After 1 minute
  • Attempt 3: After 5 minutes
  • Attempt 4: After 15 minutes
  • Attempt 5: After 1 hour

Webhooks are considered failed if:

  • Response status is not 2xx
  • Request times out (30 seconds)
  • Network error occurs

Viewing Delivery History

import { db } from "~/lib/db";
import { webhookDeliveries } from "~/lib/db/schema";
import { eq, desc } from "drizzle-orm";
 
export async function getWebhookDeliveries(webhookId: string) {
  return await db
    .select()
    .from(webhookDeliveries)
    .where(eq(webhookDeliveries.webhookId, webhookId))
    .orderBy(desc(webhookDeliveries.deliveredAt))
    .limit(50);
}

Testing Webhooks

Using Webhook.site

  1. Go to webhook.site (opens in a new tab)
  2. Copy the unique URL
  3. Create a webhook in Optare with this URL
  4. Trigger events in your app
  5. View the payloads in real-time

Local Testing with ngrok

# Install ngrok
npm install -g ngrok
 
# Start your local server
npm run dev
 
# Expose it publicly
ngrok http 3000
 
# Use the ngrok URL as your webhook endpoint
https://abc123.ngrok.io/webhooks/optare

Testing with cURL

# Simulate a webhook delivery
curl -X POST https://your-app.com/webhooks/optare \
  -H "Content-Type: application/json" \
  -H "X-Optare-Signature: your_signature" \
  -d '{
    "event": "user.created",
    "timestamp": "2025-11-11T10:30:00Z",
    "id": "evt_test",
    "data": {
      "user": {
        "id": "user_test",
        "email": "test@example.com",
        "name": "Test User"
      }
    }
  }'

Common Use Cases

1. Sync Users to External System

async function handleUserCreated(data: any) {
  // Send to Segment
  await analytics.identify(data.user.id, {
    email: data.user.email,
    name: data.user.name,
    createdAt: data.user.createdAt,
  });
 
  // Sync to Salesforce
  await salesforce.createContact({
    email: data.user.email,
    firstName: data.user.name.split(" ")[0],
    lastName: data.user.name.split(" ").slice(1).join(" "),
  });
}

2. Send Slack Notifications

async function handleMemberInvitationAccepted(data: any) {
  await fetch(process.env.SLACK_WEBHOOK_URL!, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      text: `🎉 ${data.user.name} just joined ${data.organization.name}!`,
    }),
  });
}

3. Track Analytics Events

async function handleSessionCreated(data: any) {
  await analytics.track(data.user.id, "User Signed In", {
    timestamp: data.session.createdAt,
    ip: data.session.ipAddress,
    userAgent: data.session.userAgent,
  });
}

4. Trigger Email Campaigns

async function handleUserEmailVerified(data: any) {
  // Add to email onboarding campaign
  await sendgrid.addToList({
    email: data.user.email,
    listId: "onboarding_campaign",
  });
}

Webhook Timeouts

Optare waits up to 30 seconds for your webhook endpoint to respond. If your processing takes longer:

// ❌ Bad - Processing in the handler
export async function action({ request }: ActionFunctionArgs) {
  const event = await request.json();
  
  // This might take too long!
  await processLargeData(event);
  
  return json({ received: true });
}
 
// ✅ Good - Queue for async processing
export async function action({ request }: ActionFunctionArgs) {
  const event = await request.json();
  
  // Queue for background processing
  await queue.add("webhook", event);
  
  // Respond immediately
  return json({ received: true });
}

Disabling/Enabling Webhooks

import { db } from "~/lib/db";
import { webhooks } from "~/lib/db/schema";
import { eq } from "drizzle-orm";
 
export async function toggleWebhook(
  webhookId: string,
  isActive: boolean
) {
  await db
    .update(webhooks)
    .set({ isActive })
    .where(eq(webhooks.id, webhookId));
}

Troubleshooting

Webhook Not Receiving Events

  1. Check webhook is active - Ensure isActive is true
  2. Verify URL is accessible - Test with cURL
  3. Check event subscriptions - Ensure you're subscribed to the event
  4. Review delivery logs - Check for failed deliveries
  5. Verify firewall settings - Allow Optare's IP addresses

Invalid Signature Errors

  1. Use raw body - Don't parse JSON before verifying signature
  2. Check secret - Ensure you're using the correct webhook secret
  3. Verify HMAC algorithm - Use SHA-256, not SHA-1

Duplicate Events

Webhooks may be delivered more than once. Use the event id to deduplicate:

const processedEvents = new Set<string>();
 
export async function handleWebhook(event: any) {
  if (processedEvents.has(event.id)) {
    console.log("Duplicate event, skipping");
    return;
  }
 
  processedEvents.add(event.id);
  
  // Process event...
}

Next: Audit Logs →