Skip to main content

Webhooks

Somiti sends HTTP POST requests to your server when something changes in your community. You register a URL, pick the events you care about, and Somiti delivers a JSON payload every time one of those events fires.

Set up a webhook

  1. Go to Settings > Integrations and scroll to the Webhooks section.
  2. Click Add Webhook.
  3. Fill in the fields:
    • Name (required, up to 100 characters)
    • URL (required, must be HTTPS)
    • Description (optional)
  4. Check the events you want to receive. They’re grouped by category (members, memberships, events, event registrations, payments, announcements).
  5. Choose a status: Active or Inactive.
  6. Click Save.

Somiti generates a 64-character signing secret automatically. You’ll see it on the webhook’s detail page. Copy it and store it somewhere safe because you’ll need it to verify signatures.

Note: Your endpoint must use HTTPS. Somiti won’t deliver to plain HTTP URLs.

Test a webhook

After you’ve created a webhook, you can send a test delivery to make sure your endpoint is reachable.

  1. Go to Settings > Integrations > Webhooks and click the webhook you want to test.
  2. Click Test at the top of the detail page.

Somiti sends a synchronous POST to your URL with this payload:

{
  "test": true,
  "message": "This is a test webhook from Somiti",
  "community": "Your Community Name",
  "timestamp": "2024-01-15T10:30:00Z"
}

If your endpoint returns a 2xx response, you’ll see a success message. If something goes wrong, the error appears on the page so you can debug it.

Regenerate your signing secret

If your secret is compromised, you can generate a new one without deleting the webhook.

  1. Go to Settings > Integrations > Webhooks and click the webhook.
  2. Click Regenerate Secret.
  3. Copy the new secret and update it in your application.

After you regenerate, the old secret stops working immediately. Any deliveries signed with the old secret won’t pass signature verification.

Verify webhook signatures

Somiti signs every delivery with HMAC-SHA256 so you can confirm the request actually came from Somiti and hasn’t been tampered with.

The signature covers the timestamp and the payload body:

HMAC-SHA256(secret, timestamp + "." + payload)

The result is sent in the X-Webhook-Signature header as sha256=<hex-encoded-signature>.

To verify:

  1. Extract the X-Webhook-Timestamp and X-Webhook-Signature headers.
  2. Get the raw request body (the JSON payload).
  3. Concatenate: timestamp + "." + payload.
  4. Compute HMAC-SHA256 using your webhook secret.
  5. Compare with the provided signature using constant-time comparison.
  6. Optionally, reject requests with timestamps older than 5 minutes to prevent replay attacks.

Ruby

require 'openssl'

def verify_webhook(request, secret)
  timestamp = request.headers['X-Webhook-Timestamp']
  signature = request.headers['X-Webhook-Signature']
  payload = request.body.read

  # Check timestamp to prevent replay attacks
  request_time = Time.at(timestamp.to_i)
  if (Time.current - request_time).abs > 300 # 5 minutes
    return false
  end

  # Compute expected signature
  signature_payload = "#{timestamp}.#{payload}"
  expected = "sha256=" + OpenSSL::HMAC.hexdigest(
    OpenSSL::Digest.new('sha256'),
    secret,
    signature_payload
  )

  # Constant-time comparison
  ActiveSupport::SecurityUtils.secure_compare(expected, signature)
end

Node.js

const crypto = require('crypto');
const express = require('express');

// Capture the raw body before Express parses it.
// Add this middleware BEFORE your webhook route.
app.use('/webhooks', express.json({
  verify: (req, res, buf) => { req.rawBody = buf.toString('utf-8'); }
}));

function verifyWebhook(req, secret) {
  const timestamp = req.headers['x-webhook-timestamp'];
  const signature = req.headers['x-webhook-signature'];
  const payload = req.rawBody;

  // Check timestamp to prevent replay attacks
  const requestTime = new Date(parseInt(timestamp) * 1000);
  const now = new Date();
  if (Math.abs(now - requestTime) > 300000) { // 5 minutes
    return false;
  }

  // Compute expected signature
  const signaturePayload = `${timestamp}.${payload}`;
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(signaturePayload)
    .digest('hex');

  // Constant-time comparison
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature)
  );
}

Python

import hmac
import hashlib
import time

def verify_webhook(request, secret):
    timestamp = request.headers.get('X-Webhook-Timestamp')
    signature = request.headers.get('X-Webhook-Signature')
    payload = request.data.decode('utf-8')

    # Check timestamp to prevent replay attacks
    request_time = int(timestamp)
    current_time = int(time.time())
    if abs(current_time - request_time) > 300:  # 5 minutes
        return False

    # Compute expected signature
    signature_payload = f"{timestamp}.{payload}"
    expected = "sha256=" + hmac.new(
        secret.encode('utf-8'),
        signature_payload.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()

    # Constant-time comparison
    return hmac.compare_digest(expected, signature)

Event reference

Somiti supports 20 webhook events across 6 categories.

Member events

Event Description
member.created A new member is added to the community
member.updated A member’s profile is updated
member.deleted A member is removed from the community
member.status_changed A member’s status changes (active, inactive, suspended)

Membership events

Event Description
membership.created A new membership is created
membership.activated A membership becomes active
membership.suspended A membership is suspended
membership.cancelled A membership is cancelled
membership.expired A membership expires
membership.renewed A membership is renewed

Event events

Event Description
event.created A new event is created
event.updated An event is updated
event.published An event is published
event.cancelled An event is cancelled

Event registration events

Event Description
event_registration.created Someone registers for an event
event_registration.cancelled An event registration is cancelled
event_registration.attended An attendee is checked in

Payment events

Event Description
payment.completed A payment is processed successfully
payment.failed A payment fails
payment.refunded A payment is refunded

Announcement events

Event Description
announcement.published An announcement is published

Payload structure

Every webhook payload follows the same envelope structure:

{
  "id": "evt_a1b2c3d4e5f6g7h8i9j0k1l2",
  "type": "member.created",
  "created_at": "2024-01-15T10:30:00Z",
  "data": {
    // Event-specific data
  }
}

Envelope fields

Field Type Description
id string Unique event identifier (format: evt_ prefix + 24 hex characters)
type string The event type (e.g., member.created)
created_at string ISO 8601 timestamp of when the event was created
data object Event-specific payload data

Example: member payload

Sent with member.created, member.updated, member.deleted, and member.status_changed events:

{
  "id": "evt_a1b2c3d4e5f6g7h8i9j0k1l2",
  "type": "member.created",
  "created_at": "2024-01-15T10:30:00Z",
  "data": {
    "id": 12345,
    "email": "[email protected]",
    "name": "Jane Doe",
    "status": "active",
    "role": "member",
    "phone": "+1-555-123-4567",
    "created_at": "2024-01-15T10:30:00Z",
    "updated_at": "2024-01-15T10:30:00Z"
  }
}

Example: membership payload

Sent with all membership.* events:

{
  "id": "evt_b2c3d4e5f6g7h8i9j0k1l2m3",
  "type": "membership.activated",
  "created_at": "2024-01-15T10:30:00Z",
  "data": {
    "id": 67890,
    "user_id": 12345,
    "tier_id": 1,
    "tier_name": "Premium",
    "status": "active",
    "start_date": "2024-01-15",
    "end_date": "2025-01-15",
    "auto_renew": true,
    "created_at": "2024-01-15T10:30:00Z",
    "updated_at": "2024-01-15T10:30:00Z",
    "user": {
      "id": 12345,
      "email": "[email protected]",
      "name": "Jane Doe"
    }
  }
}

Example: event payload

Sent with all event.* events:

{
  "id": "evt_c3d4e5f6g7h8i9j0k1l2m3n4",
  "type": "event.published",
  "created_at": "2024-01-15T10:30:00Z",
  "data": {
    "id": 101,
    "title": "Annual Community Meetup",
    "description": "Join us for our annual gathering...",
    "category": "meetup",
    "status": "published",
    "start_datetime": "2024-03-15T18:00:00Z",
    "end_datetime": "2024-03-15T21:00:00Z",
    "location_type": "in_person",
    "venue_name": "Community Center",
    "max_capacity": 100,
    "registration_count": 45,
    "created_at": "2024-01-10T09:00:00Z",
    "updated_at": "2024-01-15T10:30:00Z"
  }
}

Example: event registration payload

Sent with all event_registration.* events:

{
  "id": "evt_d4e5f6g7h8i9j0k1l2m3n4o5",
  "type": "event_registration.created",
  "created_at": "2024-01-15T10:30:00Z",
  "data": {
    "id": 5001,
    "event_id": 101,
    "user_id": 12345,
    "status": "confirmed",
    "payment_status": "paid",
    "amount_paid_cents": 2500,
    "registered_at": "2024-01-15T10:30:00Z",
    "event": {
      "id": 101,
      "title": "Annual Community Meetup"
    },
    "user": {
      "id": 12345,
      "email": "[email protected]",
      "name": "Jane Doe"
    }
  }
}

Example: payment payload

Sent with all payment.* events:

{
  "id": "evt_e5f6g7h8i9j0k1l2m3n4o5p6",
  "type": "payment.completed",
  "created_at": "2024-01-15T10:30:00Z",
  "data": {
    "id": 8001,
    "amount_cents": 9900,
    "currency": "usd",
    "payment_type": "membership",
    "processor": "stripe",
    "created_at": "2024-01-15T10:30:00Z"
  }
}

Example: announcement payload

Sent with announcement.published event:

{
  "id": "evt_f6g7h8i9j0k1l2m3n4o5p6q7",
  "type": "announcement.published",
  "created_at": "2024-01-15T10:30:00Z",
  "data": {
    "id": 201,
    "title": "Important Update",
    "content": "We have some exciting news to share...",
    "target_audience": "all_members",
    "published_at": "2024-01-15T10:30:00Z",
    "expires_at": "2024-02-15T10:30:00Z",
    "created_at": "2024-01-15T09:00:00Z"
  }
}

HTTP headers

Every webhook delivery includes these headers:

Header Description
Content-Type application/json
User-Agent Somiti-Webhooks/1.0
X-Webhook-Id The ID of the webhook configuration
X-Webhook-Event The event type (e.g., member.created)
X-Webhook-Delivery Unique ID for this delivery attempt
X-Webhook-Timestamp Unix timestamp when the request was sent
X-Webhook-Signature HMAC-SHA256 signature for verification (see Verify webhook signatures)

Retry policy

If your endpoint returns a non-2xx status code or doesn’t respond within 30 seconds, Somiti retries the delivery using exponential backoff.

The delay between retries follows the formula (5^attempt) * 60 seconds, clamped between 30 seconds and 1 hour. Somiti keeps retrying until it hits the maximum number of attempts.

Each delivery has one of four statuses:

Status Meaning
pending Queued, hasn’t been attempted yet
success Your endpoint returned a 2xx response
retrying Failed, but Somiti will try again
failed All retry attempts exhausted

You can view delivery history on the webhook’s detail page. The page shows total deliveries, success rate, when the webhook last fired, and a table of recent deliveries with event type, status, response code, duration, and timestamp.

Tip: Your endpoint should return a 2xx response as quickly as possible. If you need to do heavy processing, acknowledge the webhook first and handle the work in a background job.

Troubleshooting

Webhook isn’t firing

  • Check that the webhook status is Active on the detail page.
  • Make sure you’ve checked at least one event.
  • Click Test to confirm your endpoint is reachable.

Signature verification failing

  • Confirm you’re using the current signing secret. If you regenerated it recently, update your application with the new one.
  • Make sure you’re using the raw request body, not a parsed-and-reserialized version. Reserialization can change whitespace or key order.
  • Check that you’re concatenating the timestamp and payload with a . separator.

Deliveries showing as failed

  • Look at the response code in the delivery table. A 500 means your server hit an error; a timeout means it took longer than 30 seconds.
  • If you see retrying, Somiti hasn’t given up yet. Check back after the next attempt.

Duplicate events

  • Webhooks can occasionally deliver the same event more than once. Use the event id field to deduplicate. Store processed event IDs and skip any you’ve already handled.