# Webhook Integration Guide

### Purpose

This webhook is triggered **each time a new video recording is received** in your Birdie workspace. It allows you to automate workflows, store references, or trigger actions when a video becomes available.

***

### Webhook Delivery

* **Method:** `POST`
* **Protocol:** `HTTPS` only
* **Content-Type:** `application/json`

Birdie sends a signed POST request to the endpoint URL you register. The JSON body contains data about the received video.

***

### Get Started

Head to your Birdie workspace settings page and enable [Receive recordings via a Webhook](https://app.birdie.so/settings#forward). \
You must validate your URL endpoint by returning a [valid response](#webhook-response-requirements) before saving.\
From there copy your Singing Secret to setup [Signature Verification](#signature-verification).&#x20;

***

### Signature Verification

To ensure the webhook is authentic, each request includes a `Signature` HTTP header. This is a HMAC-SHA256 hash of the JSON payload using your Signing Secret.

```http
Signature: <HMAC_SHA256_SIGNATURE>
```

#### Hash generation formula:

```
HMAC_SHA256(<payload>, your_signing_secret)
```

#### Verifying Signature - Code Examples

Hash the Json Payload with your Signing Secret, and make sure it match the `Signature` Header. &#x20;

{% tabs %}
{% tab title="Node.js (Express)" %}

```js
const crypto = require('crypto');

app.post('/webhook', express.json(), (req, res) => {
  const signature = req.header('Signature');
  const payload = JSON.stringify(req.body);
  const expected = crypto
    .createHmac('sha256', process.env.WEBHOOK_SECRET)
    .update(payload)
    .digest('hex');

  if (signature !== expected) {
    return res.status(401).send('Invalid signature');
  }

  res.send('ok');
});
```

{% endtab %}

{% tab title="PHP (Laravel) " %}

```php
$payloadJson = file_get_contents('php://input');
$signature = hash_hmac('sha256', $payloadJson, '<your_secret>');
$providedSignature = $request->header('Signature');

if ($signature !== $providedSignature) {
    return response('Invalid signature', 401);
}

return response('ok', 200);
```

{% endtab %}

{% tab title="Python (Flask)" %}

```python
import hmac, hashlib
from flask import Flask, request

app = Flask(__name__)

@app.route('/webhook', methods=['POST'])
def webhook():
    secret = b'your_secret_here'
    payload = request.get_data()
    provided = request.headers.get('Signature')
    expected = hmac.new(secret, payload, hashlib.sha256).hexdigest()

    if provided != expected:
        return 'Invalid signature', 401

    return 'ok', 200
```

{% endtab %}

{% tab title="Ruby (Sinatra)" %}

```ruby
require 'sinatra'
require 'openssl'

post '/webhook' do
  payload = request.body.read
  signature = request.env['HTTP_SIGNATURE']
  expected = OpenSSL::HMAC.hexdigest('sha256', ENV['WEBHOOK_SECRET'], payload)

  halt 401, 'Invalid signature' unless Rack::Utils.secure_compare(expected, signature)
  'ok'
end
```

{% endtab %}

{% tab title="Go" %}

```go
import (
  "crypto/hmac"
  "crypto/sha256"
  "encoding/hex"
  "io/ioutil"
  "net/http"
)

func webhookHandler(w http.ResponseWriter, r *http.Request) {
  body, _ := ioutil.ReadAll(r.Body)
  signature := r.Header.Get("Signature")

  mac := hmac.New(sha256.New, []byte("your_secret"))
  mac.Write(body)
  expected := hex.EncodeToString(mac.Sum(nil))

  if signature != expected {
    http.Error(w, "Invalid signature", http.StatusUnauthorized)
    return
  }

  w.Write([]byte("ok"))
}
```

{% endtab %}

{% tab title=".NET (ASP.NET Core)" %}

```csharp
[HttpPost("webhook")]
public IActionResult Webhook([FromBody] JObject body) {
    var secret = Encoding.UTF8.GetBytes("your_secret");
    var payload = Request.Body;

    using var reader = new StreamReader(payload);
    var bodyStr = reader.ReadToEnd();
    var hmac = new HMACSHA256(secret);
    var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(bodyStr));
    var expected = BitConverter.ToString(hash).Replace("-", "").ToLower();

    var signature = Request.Headers["Signature"].ToString();
    if (expected != signature) return Unauthorized("Invalid signature");

    return Ok("ok");
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
For additional security layer you can also verify the IP address from Birdie servers: \
If you want to go this way, allow calls coming from `18.189.92.93` and `3.20.198.186`
{% endhint %}

***

### Webhook Response Requirements

Your webhook handler must:

* Return an HTTP status code **between 200 and 299** to confirm successful delivery.
* Avoid redirects, errors, or timeouts — otherwise the attempt is marked as failed.

***

### Retry Policy & Backoff

Birdie uses exponential backoff for failed webhook deliveries. Each webhook will be retried up to **3 times**, as follows:

1. **1st attempt:** immediately
2. **2nd attempt:** 10 seconds later
3. **3rd attempt:** 100 seconds later

If all attempts fail, the webhook is marked as permanently failed.

***

### Auto-Disabling Policy

To prevent loops or spam, Birdie disables your webhook endpoint **automatically** after **10 consecutive failures**. You will be able to re-enable the webhook in your Birdie dashboard.

***

### Need Help?

You can use any webhook testing service of your choice to test and introspect Birdie calls, for example <https://webhook.site>

If needed, email us at <support@birdie.so> we’ll help you resolve it quickly.
