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 registereduser.updated- User profile changeduser.deleted- User account deleteduser.email.verified- Email verification completeduser.password.changed- Password was changed
Session Events
session.created- User signed insession.revoked- Session was terminatedsession.expired- Session naturally expired
Organization Events
organization.created- New organization createdorganization.updated- Organization settings changedorganization.deleted- Organization was deleted
Member Events
member.added- User added to organizationmember.removed- User removed from organizationmember.role.changed- Member role updatedmember.invitation.sent- Invitation email sentmember.invitation.accepted- Invitation acceptedmember.invitation.expired- Invitation expired
OAuth Events
oauth.client.created- OAuth client registeredoauth.client.deleted- OAuth client removedoauth.authorization.granted- User authorized an appoauth.token.issued- Access token generated
Security Events
2fa.enabled- Two-factor authentication enabled2fa.disabled- Two-factor authentication disabledapi_key.created- API key generatedapi_key.revoked- API key revoked
Creating a Webhook
Via Dashboard
- Navigate to Settings → Webhooks
- Click Create Webhook
- Enter your webhook URL
- Select the events you want to receive
- 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
- Always verify signatures - Never trust incoming webhooks without verification
- Use HTTPS endpoints - Only use HTTPS URLs for webhook endpoints
- Return quickly - Respond with 200 OK quickly, then process asynchronously
- Implement retries - Handle webhook failures gracefully
- Log everything - Keep audit logs of all received webhooks
- 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
- Go to webhook.site (opens in a new tab)
- Copy the unique URL
- Create a webhook in Optare with this URL
- Trigger events in your app
- 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/optareTesting 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
- Check webhook is active - Ensure
isActiveis true - Verify URL is accessible - Test with cURL
- Check event subscriptions - Ensure you're subscribed to the event
- Review delivery logs - Check for failed deliveries
- Verify firewall settings - Allow Optare's IP addresses
Invalid Signature Errors
- Use raw body - Don't parse JSON before verifying signature
- Check secret - Ensure you're using the correct webhook secret
- 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 →