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
- Go to Settings > Integrations and scroll to the Webhooks section.
- Click Add Webhook.
- Fill in the fields:
- Name (required, up to 100 characters)
- URL (required, must be HTTPS)
- Description (optional)
- Check the events you want to receive. They’re grouped by category (members, memberships, events, event registrations, payments, announcements).
- Choose a status: Active or Inactive.
- 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.
- Go to Settings > Integrations > Webhooks and click the webhook you want to test.
- 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.
- Go to Settings > Integrations > Webhooks and click the webhook.
- Click Regenerate Secret.
- 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:
- Extract the
X-Webhook-TimestampandX-Webhook-Signatureheaders. - Get the raw request body (the JSON payload).
- Concatenate:
timestamp + "." + payload. - Compute HMAC-SHA256 using your webhook secret.
- Compare with the provided signature using constant-time comparison.
- 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
idfield to deduplicate. Store processed event IDs and skip any you’ve already handled.
Related docs
- Slack integration - Post announcements and event updates to a Slack channel
- Notifications - Control in-app and email notifications
- Settings - All community settings