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

# Doohick Webhooks API: Register Endpoints and Verify Events

> Create and manage Doohick webhooks via the REST API. Subscribe to platform events and verify delivery signatures to keep your integrations secure.

Webhooks let your application receive real-time notifications when events happen inside Doohick. Instead of polling the API, you register a URL and Doohick sends an HTTP `POST` request to that URL whenever a subscribed event occurs. Use this API to create, list, and delete webhook endpoints, and follow the signature-verification guide below to ensure every delivery is authentic.

***

## Webhook events

Subscribe to one or more of the following event types when you create a webhook. Doohick delivers a payload to your endpoint each time any subscribed event fires.

| Event                       | Description                                               |
| --------------------------- | --------------------------------------------------------- |
| `resource.created`          | A new resource was created in the workspace.              |
| `resource.updated`          | An existing resource was updated.                         |
| `resource.deleted`          | A resource was permanently deleted.                       |
| `integration.connected`     | A third-party integration was successfully connected.     |
| `integration.failed`        | A third-party integration encountered a connection error. |
| `member.invited`            | A new member was invited to the workspace.                |
| `billing.payment_succeeded` | A billing payment was processed successfully.             |
| `billing.payment_failed`    | A billing payment attempt failed.                         |

***

## `POST /webhooks`

Create a new webhook endpoint. Supply the destination URL and the list of events you want to subscribe to. Doohick returns a `secret` in the response — store this securely, as you will need it to verify incoming signatures.

<ParamField body="url" type="string" required>
  The HTTPS URL Doohick should send event payloads to. Must be a publicly reachable endpoint.
</ParamField>

<ParamField body="events" type="array" required>
  An array of event type strings to subscribe to. Pass `["*"]` to subscribe to all current and future events.
</ParamField>

<ParamField body="description" type="string">
  An optional human-readable label to help you identify this webhook endpoint in the dashboard.
</ParamField>

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.doohick.com/v1/webhooks \
    -H "Authorization: Bearer dh_live_XXXXXXXXXXXXXXXXXXXX" \
    -H "Content-Type: application/json" \
    -d '{"url": "https://yourapp.com/hooks/doohick", "events": ["resource.created", "resource.updated"]}'
  ```

  ```javascript Node.js theme={null}
  const response = await fetch('https://api.doohick.com/v1/webhooks', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer dh_live_XXXXXXXXXXXXXXXXXXXX',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      url: 'https://yourapp.com/hooks/doohick',
      events: ['resource.created', 'resource.updated'],
      description: 'Production resource sync'
    })
  });

  const { data } = await response.json();
  console.log('Webhook secret:', data.secret);
  ```

  ```python Python theme={null}
  import requests

  response = requests.post(
      'https://api.doohick.com/v1/webhooks',
      headers={
          'Authorization': 'Bearer dh_live_XXXXXXXXXXXXXXXXXXXX',
          'Content-Type': 'application/json'
      },
      json={
          'url': 'https://yourapp.com/hooks/doohick',
          'events': ['resource.created', 'resource.updated'],
          'description': 'Production resource sync'
      }
  )

  data = response.json()['data']
  print('Webhook secret:', data['secret'])
  ```
</CodeGroup>

**Response `201 Created`**

```json JSON theme={null}
{
  "data": {
    "id": "wh_abc123",
    "url": "https://yourapp.com/hooks/doohick",
    "events": ["resource.created", "resource.updated"],
    "description": "Production resource sync",
    "secret": "whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "status": "active",
    "workspace_id": "ws_xyz789",
    "created_at": "2024-01-15T10:00:00Z",
    "updated_at": "2024-01-15T10:00:00Z"
  }
}
```

<ResponseField name="data" type="object" required>
  The newly created webhook object.

  <Expandable title="data">
    <ResponseField name="id" type="string">
      Unique identifier for the webhook, prefixed with `wh_`.
    </ResponseField>

    <ResponseField name="url" type="string">
      The destination URL registered for this webhook.
    </ResponseField>

    <ResponseField name="events" type="array">
      The list of event types this webhook is subscribed to.
    </ResponseField>

    <ResponseField name="description" type="string">
      The optional human-readable label for this endpoint.
    </ResponseField>

    <ResponseField name="secret" type="string">
      The signing secret used to generate `X-Doohick-Signature` headers. This value is only returned once at creation time — store it securely.
    </ResponseField>

    <ResponseField name="status" type="string">
      Current status of the webhook endpoint. One of `active` or `disabled`.
    </ResponseField>

    <ResponseField name="workspace_id" type="string">
      The ID of the workspace that owns this webhook.
    </ResponseField>

    <ResponseField name="created_at" type="string">
      ISO 8601 timestamp indicating when the webhook was registered.
    </ResponseField>

    <ResponseField name="updated_at" type="string">
      ISO 8601 timestamp indicating when the webhook was last modified.
    </ResponseField>
  </Expandable>
</ResponseField>

<Warning>
  The `secret` field is returned **only once** at creation time and is never exposed again through the API. Save it to a secure secrets store immediately — if you lose it, you must delete and recreate the webhook.
</Warning>

***

## `GET /webhooks`

List all webhook endpoints registered in your workspace. Results are paginated.

<ParamField query="limit" type="integer" default="20">
  The maximum number of webhooks to return. Accepted range is `1`–`100`.
</ParamField>

<ParamField query="offset" type="integer" default="0">
  The number of webhooks to skip before returning results.
</ParamField>

<CodeGroup>
  ```bash cURL theme={null}
  curl -X GET "https://api.doohick.com/v1/webhooks?limit=20&offset=0" \
    -H "Authorization: Bearer dh_live_XXXXXXXXXXXXXXXXXXXX"
  ```

  ```javascript Node.js theme={null}
  const response = await fetch('https://api.doohick.com/v1/webhooks?limit=20&offset=0', {
    headers: { 'Authorization': 'Bearer dh_live_XXXXXXXXXXXXXXXXXXXX' }
  });

  const { data, meta } = await response.json();
  console.log(`${meta.total} webhooks registered`);
  ```

  ```python Python theme={null}
  import requests

  response = requests.get(
      'https://api.doohick.com/v1/webhooks',
      headers={'Authorization': 'Bearer dh_live_XXXXXXXXXXXXXXXXXXXX'},
      params={'limit': 20, 'offset': 0}
  )

  result = response.json()
  print(f"{result['meta']['total']} webhooks registered")
  ```
</CodeGroup>

**Response `200 OK`**

```json JSON theme={null}
{
  "data": [
    {
      "id": "wh_abc123",
      "url": "https://yourapp.com/hooks/doohick",
      "events": ["resource.created", "resource.updated"],
      "description": "Production resource sync",
      "status": "active",
      "workspace_id": "ws_xyz789",
      "created_at": "2024-01-15T10:00:00Z",
      "updated_at": "2024-01-15T10:00:00Z"
    }
  ],
  "meta": {
    "total": 3,
    "limit": 20,
    "offset": 0
  }
}
```

<Note>
  The `secret` field is omitted from list responses. Only the creation response includes the secret.
</Note>

***

## `DELETE /webhooks/{id}`

Delete a webhook endpoint by its ID. Doohick immediately stops delivering events to the registered URL. A successful request returns `204 No Content` with an empty body.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X DELETE https://api.doohick.com/v1/webhooks/wh_abc123 \
    -H "Authorization: Bearer dh_live_XXXXXXXXXXXXXXXXXXXX"
  ```

  ```javascript Node.js theme={null}
  const response = await fetch('https://api.doohick.com/v1/webhooks/wh_abc123', {
    method: 'DELETE',
    headers: { 'Authorization': 'Bearer dh_live_XXXXXXXXXXXXXXXXXXXX' }
  });

  if (response.status === 204) {
    console.log('Webhook deleted successfully.');
  }
  ```

  ```python Python theme={null}
  import requests

  response = requests.delete(
      'https://api.doohick.com/v1/webhooks/wh_abc123',
      headers={'Authorization': 'Bearer dh_live_XXXXXXXXXXXXXXXXXXXX'}
  )

  if response.status_code == 204:
      print('Webhook deleted successfully.')
  ```
</CodeGroup>

**Response `204 No Content`**

The response body is empty on success.

***

## Verifying webhook signatures

Every webhook delivery from Doohick includes an `X-Doohick-Signature` header containing an HMAC-SHA256 hex digest of the raw request body, signed with your webhook's `secret`. Always verify this signature before processing any incoming payload to ensure the request originates from Doohick and has not been tampered with.

<Warning>
  Always verify the `X-Doohick-Signature` header before trusting or acting on a webhook payload. Processing unverified payloads exposes your application to spoofed or replayed requests.
</Warning>

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

  function verifyWebhook(payload, signature, secret) {
    const expected = crypto
      .createHmac('sha256', secret)
      .update(payload, 'utf8')
      .digest('hex');
    return crypto.timingSafeEqual(
      Buffer.from(expected),
      Buffer.from(signature)
    );
  }

  // Express.js example
  app.post('/hooks/doohick', express.raw({ type: 'application/json' }), (req, res) => {
    const signature = req.headers['x-doohick-signature'];
    const isValid = verifyWebhook(req.body, signature, process.env.DOOHICK_WEBHOOK_SECRET);

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

    const event = JSON.parse(req.body);
    console.log('Received event:', event.type);
    res.status(200).send('OK');
  });
  ```

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

  def verify_webhook(payload: bytes, signature: str, secret: str) -> bool:
      expected = hmac.new(
          secret.encode(),
          payload,
          hashlib.sha256
      ).hexdigest()
      return hmac.compare_digest(expected, signature)

  # Flask example
  from flask import Flask, request, abort
  import os

  app = Flask(__name__)

  @app.route('/hooks/doohick', methods=['POST'])
  def handle_webhook():
      signature = request.headers.get('X-Doohick-Signature', '')
      is_valid = verify_webhook(
          request.get_data(),
          signature,
          os.environ['DOOHICK_WEBHOOK_SECRET']
      )

      if not is_valid:
          abort(401)

      event = request.get_json()
      print('Received event:', event['type'])
      return 'OK', 200
  ```
</CodeGroup>

<Tip>
  Test your webhook handler locally using a tunneling tool like [ngrok](https://ngrok.com). Run `ngrok http 3000` to get a public HTTPS URL, then register it as your webhook endpoint while you develop.
</Tip>
