Skip to content

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:

json
{
  "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 requests
  • event_types - Array of event patterns to subscribe to

Optional Fields:

  • payload_mode - full (default) or thin; controls the level of detail in the event payload
  • secret - Signing secret for HMAC verification; if omitted, one is generated automatically
  • headers - Object of custom HTTP headers to include with every delivery
  • description - Human-readable label for the subscription

Response (201 Created):

json
{
  "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, disabled
  • per_page (integer) - Results per page (default: 25, max: 100)
  • page (integer) - Page number

Response (200 OK):

json
{
  "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):

json
{
  "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):

json
{
  "event_types": ["product.*", "customer.*", "order.created"],
  "payload_mode": "thin",
  "headers": {
    "X-Custom-Source": "synthesq-staging"
  }
}

Response (200 OK):

json
{
  "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):

json
{
  "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):

json
{
  "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):

json
{
  "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:

PatternMatchesExample
ExactA single specific event typecustomer.created
Wildcard (entity)All events for a given entityproduct.*
Wildcard (all)Every event type in the system*

You can mix patterns in a single subscription:

json
{
  "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.

json
{
  "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.

json
{
  "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.

json
{
  "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):

json
{
  "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

  1. Read the raw request body as a byte string (do not parse or re-serialize it)
  2. Compute HMAC-SHA256 using your subscription's secret as the key and the raw body as the message
  3. Hex-encode the result
  4. Compare with the X-Webhook-Signature header value using a timing-safe comparison

Example: Node.js

javascript
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

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

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):

json
{
  "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.


Documentation for SynthesQ CRM/ERP Platform