> ## Documentation Index
> Fetch the complete documentation index at: https://docs.feedspace.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Getting Started

Feedspace provides webhooks to notify your application about important events in real-time. This section documents all available webhook events and their payloads.

Webhooks are scoped to a single workspace. A webhook only receives reviews submitted into the workspace it was created in, so if you work across multiple workspaces you need one webhook endpoint per workspace.

### How to Get Webhook Access

To get webhook access:

1. Go to the [Webhook Access](https://app.feedspace.io/automation/webhooks) page and make sure the workspace you want to receive events for is selected.
2. Enter your webhook endpoint URL in the provided input field.
3. Save the endpoint. A newly created webhook is always subscribed to **all** available events and starts out inactive, whatever you picked in the form. To narrow the subscription to specific events, edit the webhook after it has been created.
4. Click the "Activate" button to enable webhook delivery for that workspace.

Once activated, Feedspace will start sending webhook notifications for the subscribed events to your specified endpoint.

### Available Webhook Events

Feedspace currently supports the following webhook events:

* `feed.text.received`: Triggered when a new text review is received
* `feed.video.received`: Triggered when a new video review is received
* `feed.audio.received`: Triggered when a new audio review is received

### Webhook Delivery

Feedspace will send webhook notifications as HTTP POST requests to your specified webhook URL. Each notification follows this format:

```json theme={null}
{
  "type": "feed.text.received",
  "data": {},
  "reviewer_email": "reviewer@example.com"
}
```

Every payload has exactly these three top-level keys:

* `type`: The event type, one of `feed.text.received`, `feed.video.received` or `feed.audio.received`.
* `data`: The review itself. Its contents differ per event type, so it is shown empty above.
* `reviewer_email`: A copy of the email address the reviewer submitted, surfaced at the top level for convenience. It is `null` when the form did not collect an email.

For the complete `data` object, see [feed.text.received](/webhook-reference/text-reviews/received), [feed.video.received](/webhook-reference/video-reviews/received) or [feed.audio.received](/webhook-reference/audio-reviews/received).

### Delivery Behaviour

Each event is delivered as a **single** HTTP POST request. Feedspace never retries: a non-2xx status, a timeout, or a connection error will not cause the event to be sent again. The response status and body are recorded in the webhook logs. If you need at-least-once processing, queue the payload as soon as you receive it and handle failures on your side.

The request times out after 20 seconds, so acknowledge quickly and do your processing asynchronously.

## Verify Webhook Signature

To ensure the integrity and authenticity of incoming webhook requests, Feedspace signs each payload with a unique secret. We strongly recommend verifying this signature for all production webhooks.

### How It Works

1. **The Signature Headers:** Feedspace sends two headers with each webhook request:
   * `x-feedspace-signature`: The HMAC-SHA256 signature of the payload.
   * `x-feedspace-timestamp`: The Unix timestamp (in seconds) when the webhook was sent

2. **Your Signing Secret:** Each webhook in your workspace has a unique signing secret. You can find this secret in your Feedspace dashboard under **Automation > Webhook**. Production secrets are prefixed with `whsec_live_` and non-production secrets with `whsec_test_`.

3. **Verification Process:** You reconstruct the signature on your server using the timestamp, the raw request body, and your secret. If it matches the `x-feedspace-signature` header, the webhook is legitimate.

### Sample Verification Code

<CodeGroup>
  ```php PHP theme={null}
  <?php

  // 1. Retrieve the signature and timestamp from the headers
  $headers = getallheaders();
  $signature = $headers['x-feedspace-signature'] ?? null;
  $timestamp = $headers['x-feedspace-timestamp'] ?? null;

  // 2. Get the raw payload from the input stream
  $payload = file_get_contents('php://input');

  // 3. Define your secrets (Store these securely, e.g., in environment variables!)
  $secret = 'whsec_test_1234222222232s334weee';

  // 4. Compute the expected signature
  $expectedSignature = hash_hmac('sha256', $timestamp . '.' . $payload, $secret);

  // 6. Compare the signatures using a time-safe comparison
  if (hash_equals($expectedSignature, $signature)) {
      // Webhook is verified! Process the event.
  }
  ```

  ```javascript Node.js theme={null}
  const crypto = require('crypto');

  // 1. Retrieve the signature and timestamp from the headers
  const signature = req.headers['x-feedspace-signature'];
  const timestamp = req.headers['x-feedspace-timestamp'];

  // 2. Get the raw payload. Mount this route with express.raw({ type: 'application/json' })
  //    so that req.body is the unparsed Buffer. Re-serializing an already-parsed body
  //    (e.g. JSON.stringify(req.body)) will not reproduce the exact bytes that were
  //    signed, and verification will fail for legitimate webhooks.
  const payload = req.body.toString('utf8');

  // 3. Define your secret (Store securely in environment variables!)
  const secret = 'whsec_test_1234222222232s334weee';

  // 4. Compute the expected signature
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(timestamp + '.' + payload)
    .digest('hex');

  // 5. Compare the signatures using a time-safe comparison.
  //    timingSafeEqual throws if the two buffers differ in length, so compare
  //    lengths first — otherwise a malformed signature crashes the handler.
  const expectedBuffer = Buffer.from(expectedSignature, 'utf8');
  const receivedBuffer = Buffer.from(signature || '', 'utf8');

  if (
    expectedBuffer.length === receivedBuffer.length &&
    crypto.timingSafeEqual(expectedBuffer, receivedBuffer)
  ) {
      // Webhook is verified! Process the event.
  }
  ```

  ```python Python theme={null}
  import hmac
  import hashlib

  # 1. Retrieve the signature and timestamp from the headers
  signature = request.headers.get('x-feedspace-signature')
  timestamp = request.headers.get('x-feedspace-timestamp')

  # 2. Get the raw payload
  payload = request.get_data()

  # 3. Define your secret (Store securely in environment variables!)
  secret = 'whsec_test_1234222222232s334weee'

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

  # 5. Compare the signatures using a time-safe comparison
  if hmac.compare_digest(expected_signature, signature):
      # Webhook is verified! Process the event.
      pass
  ```

  ```ruby Ruby theme={null}
  require 'openssl'

  # 1. Retrieve the signature and timestamp from the headers
  signature = request.headers['x-feedspace-signature']
  timestamp = request.headers['x-feedspace-timestamp']

  # 2. Get the raw payload
  payload = request.body.read

  # 3. Define your secret (Store securely in environment variables!)
  secret = 'whsec_test_1234222222232s334weee'

  # 4. Compute the expected signature
  expected_signature = OpenSSL::HMAC.hexdigest(
    'SHA256', 
    secret, 
    "#{timestamp}.#{payload}"
  )

  # 5. Compare the signatures using a time-safe comparison
  if Rack::Utils.secure_compare(expected_signature, signature)
    # Webhook is verified! Process the event.
  end
  ```
</CodeGroup>

### Best Practices

* Implement proper error handling for incoming webhook requests
* Consider implementing request validation to verify the authenticity of webhook requests
* Handle different event types appropriately in your application
* Keep your webhook endpoint URL secure and accessible
