Managing Subscriptions
Overview
A webhook subscription tells SynthesQ which events to deliver and where to send them. Each subscription targets a single endpoint URL and can listen for one or more event patterns. When an event matches, the platform serializes the payload and queues an HTTP POST to your endpoint.
Subscriptions are scoped to your tenant. Each tenant can have up to 25 active subscriptions. If you need to route different event types to different services, create one subscription per endpoint rather than multiplexing.
API Endpoints
Create a Subscription
Endpoint: POST /api/v1/webhooks/subscriptions
Authentication: Required (Bearer token)
Request Body:
{
"url": "https://example.com/webhooks/synthesq",
"event_types": ["product.*", "customer.created"],
"payload_mode": "full",
"secret": null,
"headers": {
"X-Custom-Source": "synthesq-production"
},
"description": "Product sync for warehouse system"
}Required Fields:
url- HTTPS endpoint that will receive POST requestsevent_types- Array of event patterns to subscribe to
Optional Fields:
payload_mode-full(default) orthin; controls the level of detail in the event payloadsecret- Signing secret for HMAC verification; if omitted, one is generated automaticallyheaders- Object of custom HTTP headers to include with every deliverydescription- Human-readable label for the subscription
Response (201 Created):
{
"success": true,
"message": "Webhook subscription created successfully",
"data": {
"id": "01jwebhook123abc456def789",
"url": "https://example.com/webhooks/synthesq",
"event_types": ["product.*", "customer.created"],
"payload_mode": "full",
"secret": "whsec_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
"headers": {
"X-Custom-Source": "synthesq-production"
},
"description": "Product sync for warehouse system",
"status": "active",
"created_at": "2026-03-15T10:00:00Z",
"updated_at": "2026-03-15T10:00:00Z"
},
"meta": {}
}Save Your Secret
The secret field is only returned in the creation response. Store it securely - you will need it to verify delivery signatures. If you lose it, use the rotate-secret endpoint to generate a new one.
List Subscriptions
Endpoint: GET /api/v1/webhooks/subscriptions
Authentication: Required (Bearer token)
Query Parameters:
status(string) - Filter by status:active,disabledper_page(integer) - Results per page (default: 25, max: 100)page(integer) - Page number
Response (200 OK):
{
"success": true,
"data": [
{
"id": "01jwebhook123abc456def789",
"url": "https://example.com/webhooks/synthesq",
"event_types": ["product.*", "customer.created"],
"payload_mode": "full",
"status": "active",
"description": "Product sync for warehouse system",
"created_at": "2026-03-15T10:00:00Z",
"updated_at": "2026-03-15T10:00:00Z"
}
],
"meta": {
"current_page": 1,
"per_page": 25,
"total": 1,
"last_page": 1
},
"links": {
"first": "/api/v1/webhooks/subscriptions?page=1",
"last": "/api/v1/webhooks/subscriptions?page=1",
"prev": null,
"next": null
}
}Get Subscription Details
Endpoint: GET /api/v1/webhooks/subscriptions/{id}
Authentication: Required (Bearer token)
Response (200 OK):
{
"success": true,
"data": {
"id": "01jwebhook123abc456def789",
"url": "https://example.com/webhooks/synthesq",
"event_types": ["product.*", "customer.created"],
"payload_mode": "full",
"headers": {
"X-Custom-Source": "synthesq-production"
},
"description": "Product sync for warehouse system",
"status": "active",
"created_at": "2026-03-15T10:00:00Z",
"updated_at": "2026-03-15T10:00:00Z"
},
"meta": {}
}Update Subscription
Endpoint: PATCH /api/v1/webhooks/subscriptions/{id}
Authentication: Required (Bearer token)
Request Body (partial update allowed):
{
"event_types": ["product.*", "customer.*", "order.created"],
"payload_mode": "thin",
"headers": {
"X-Custom-Source": "synthesq-staging"
}
}Response (200 OK):
{
"success": true,
"message": "Webhook subscription updated successfully",
"data": {
"id": "01jwebhook123abc456def789",
"url": "https://example.com/webhooks/synthesq",
"event_types": ["product.*", "customer.*", "order.created"],
"payload_mode": "thin",
"status": "active",
"updated_at": "2026-03-16T14:30:00Z"
},
"meta": {}
}Delete Subscription
Soft-deletes a subscription. No further deliveries will be attempted. Existing in-flight deliveries continue to completion.
Endpoint: DELETE /api/v1/webhooks/subscriptions/{id}
Authentication: Required (Bearer token)
Response (200 OK):
{
"success": true,
"message": "Webhook subscription deleted successfully"
}Activate Subscription
Re-enables a disabled subscription. Deliveries resume immediately for new events.
Endpoint: POST /api/v1/webhooks/subscriptions/{id}/activate
Authentication: Required (Bearer token)
Response (200 OK):
{
"success": true,
"message": "Webhook subscription activated successfully",
"data": {
"id": "01jwebhook123abc456def789",
"status": "active",
"updated_at": "2026-03-20T09:00:00Z"
},
"meta": {}
}Disable Subscription
Manually disables a subscription. No new events will be delivered until re-activated.
Endpoint: POST /api/v1/webhooks/subscriptions/{id}/disable
Authentication: Required (Bearer token)
Response (200 OK):
{
"success": true,
"message": "Webhook subscription disabled successfully",
"data": {
"id": "01jwebhook123abc456def789",
"status": "disabled",
"updated_at": "2026-03-20T09:15:00Z"
},
"meta": {}
}Event Patterns
Event patterns determine which events trigger a delivery. Patterns support three matching strategies:
| Pattern | Matches | Example |
|---|---|---|
| Exact | A single specific event type | customer.created |
| Wildcard (entity) | All events for a given entity | product.* |
| Wildcard (all) | Every event type in the system | * |
You can mix patterns in a single subscription:
{
"event_types": ["product.*", "customer.created", "order.status_changed"]
}Pattern Evaluation
Patterns are evaluated with short-circuit logic. If any pattern matches, the event is delivered. Duplicate matches do not produce duplicate deliveries.
Payload Modes
Each subscription can operate in one of two payload modes. Choose based on your bandwidth and security requirements.
Full Mode
The default mode. The event payload contains the complete entity state at the time the event was emitted.
{
"id": "01jevt456abc789def012ghi3",
"event_type": "product.created",
"entity_type": "product",
"entity_id": "01jprod789abc012def345ghi6",
"occurred_at": "2026-03-15T10:30:00Z",
"payload": {
"id": "01jprod789abc012def345ghi6",
"name": "Wireless Keyboard",
"sku": "KB-WIRELESS-001",
"status": "active",
"selling_price": 49.99,
"currency": "USD",
"created_at": "2026-03-15T10:30:00Z"
}
}Thin Mode
The payload contains only the event metadata and entity identifier. Your consumer must call the SynthesQ API to fetch the full entity state.
{
"id": "01jevt456abc789def012ghi3",
"event_type": "product.created",
"entity_type": "product",
"entity_id": "01jprod789abc012def345ghi6",
"occurred_at": "2026-03-15T10:30:00Z",
"payload": null
}When to Use Thin Mode
Thin mode is useful when payloads are large, when you need to enforce access control on entity reads, or when your consumer only needs to know that something changed and can fetch the details on demand.
Custom Headers
You can attach custom HTTP headers to every delivery for a subscription. Common use cases include routing tokens, environment labels, and correlation IDs.
{
"headers": {
"X-Custom-Source": "synthesq-production",
"X-Routing-Key": "warehouse-sync",
"Authorization": "Bearer your-internal-token"
}
}Custom headers are sent alongside the standard SynthesQ delivery headers (X-Webhook-Signature, X-Webhook-Delivery-Id, Content-Type). If a custom header conflicts with a standard header, the standard header takes precedence.
Secret Management
Rotate Secret
Generates a new signing secret for the subscription. The previous secret is invalidated immediately. Update your consumer's verification logic before rotating to avoid rejected deliveries during the transition.
Endpoint: POST /api/v1/webhooks/subscriptions/{id}/rotate-secret
Authentication: Required (Bearer token)
Response (200 OK):
{
"success": true,
"message": "Webhook secret rotated successfully",
"data": {
"id": "01jwebhook123abc456def789",
"secret": "whsec_q9r8s7t6u5v4w3x2y1z0a9b8c7d6e5f4",
"updated_at": "2026-03-20T11:00:00Z"
},
"meta": {}
}Rotation Is Immediate
The old secret becomes invalid as soon as rotation completes. Any in-flight deliveries signed with the old secret will fail verification on your end. Coordinate the rotation with your consumer deployment.
Signature Verification
Every push delivery includes an X-Webhook-Signature header containing an HMAC-SHA256 signature computed from the raw request body and your subscription's signing secret. Verifying this signature confirms that the payload originated from SynthesQ and was not modified in transit.
Verification Steps
- Read the raw request body as a byte string (do not parse or re-serialize it)
- Compute HMAC-SHA256 using your subscription's secret as the key and the raw body as the message
- Hex-encode the result
- Compare with the
X-Webhook-Signatureheader value using a timing-safe comparison
Example: Node.js
const crypto = require('crypto');
function verifySignature(rawBody, secret, signatureHeader) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody, 'utf8')
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected, 'hex'),
Buffer.from(signatureHeader, 'hex')
);
}
// In your webhook handler:
app.post('/webhooks/synthesq', (req, res) => {
const isValid = verifySignature(
req.rawBody,
process.env.WEBHOOK_SECRET,
req.headers['x-webhook-signature']
);
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Process the event...
res.status(200).json({ received: true });
});Example: PHP
function verifySignature(string $rawBody, string $secret, string $signatureHeader): bool
{
$expected = hash_hmac('sha256', $rawBody, $secret);
return hash_equals($expected, $signatureHeader);
}
// In your webhook controller:
$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
if (! verifySignature($rawBody, $webhookSecret, $signature)) {
http_response_code(401);
echo json_encode(['error' => 'Invalid signature']);
exit;
}
// Process the event...
http_response_code(200);
echo json_encode(['received' => true]);Example: Python
import hashlib
import hmac
def verify_signature(raw_body: bytes, secret: str, signature_header: str) -> bool:
expected = hmac.new(
secret.encode('utf-8'),
raw_body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature_header)Always Use Timing-Safe Comparison
Never use == to compare signatures. Use crypto.timingSafeEqual (Node.js), hash_equals (PHP), or hmac.compare_digest (Python) to prevent timing attacks.
Testing a Subscription
Send a test event to verify that your endpoint is reachable and correctly processing deliveries. The test event uses the webhook.test event type and does not correspond to a real domain event.
Endpoint: POST /api/v1/webhooks/subscriptions/{id}/test
Authentication: Required (Bearer token)
Response (200 OK):
{
"success": true,
"message": "Test event dispatched successfully",
"data": {
"delivery_id": "01jdlvr789abc012def345ghi6",
"event_type": "webhook.test",
"status": "pending"
},
"meta": {}
}The test delivery follows the same retry and signature logic as production deliveries. Check the delivery status via GET /api/v1/webhooks/subscriptions/{id}/deliveries to confirm your endpoint received it.
Related Documentation
- Event Feed - Poll for events using cursor-based pagination
- Delivery & Reliability - Retry strategy, auto-disable, and delivery inspection
- Webhooks Overview - Module architecture and quick start