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

# mFoundry Webhooks API: Register, List, and Delete Endpoints

> Create, list, and delete mFoundry webhook subscriptions via the API. Includes available events, payload structure, and signature verification.

Webhooks let mFoundry push real-time event notifications to your own infrastructure the moment something meaningful happens — a project is created, a workflow completes, or a task is assigned. The Webhooks API lets you register, inspect, and remove webhook subscriptions programmatically, without touching the dashboard. All webhook endpoints sit under `/v1/webhooks`.

***

## GET /v1/webhooks

List all webhook subscriptions registered in your organization. Use this endpoint to audit active subscriptions or retrieve webhook IDs for deletion.

**Example response:**

```json theme={null}
{
  "data": [
    {
      "id": "wh_abc123",
      "url": "https://myapp.example.com/hooks/mfoundry",
      "events": ["workflow.completed", "project.created"],
      "active": true,
      "created_at": "2024-03-01T12:00:00Z"
    }
  ],
  "next_cursor": null
}
```

<CodeGroup>
  ```bash cURL theme={null}
  curl --request GET \
    --url https://api.mfoundry.io/v1/webhooks \
    --header "Authorization: Bearer YOUR_API_KEY"
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch("https://api.mfoundry.io/v1/webhooks", {
    headers: { "Authorization": "Bearer YOUR_API_KEY" },
  });
  const { data } = await response.json();
  ```
</CodeGroup>

***

## POST /v1/webhooks

Register a new webhook subscription. mFoundry will send an HTTP `POST` request to your specified URL each time one of the subscribed events fires.

<ParamField body="url" type="string" required>
  The destination URL that mFoundry will deliver event payloads to. Must use `https://` — plaintext HTTP endpoints are not accepted.
</ParamField>

<ParamField body="events" type="array" required>
  An array of event type strings to subscribe to. You must provide at least one event. Example: `["workflow.completed", "project.created"]`. See the [Available Events](#available-events) section below for all valid values.
</ParamField>

<ParamField body="secret" type="string">
  A shared secret used to sign outgoing webhook payloads. If you omit this field, mFoundry auto-generates a cryptographically secure secret for you. The secret is returned **once** in the creation response and cannot be retrieved again.
</ParamField>

<CodeGroup>
  ```bash cURL theme={null}
  curl --request POST \
    --url https://api.mfoundry.io/v1/webhooks \
    --header "Authorization: Bearer YOUR_API_KEY" \
    --header "Content-Type: application/json" \
    --data '{
      "url": "https://myapp.example.com/hooks/mfoundry",
      "events": ["workflow.completed", "project.created"],
      "secret": "my-super-secret-value"
    }'
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch("https://api.mfoundry.io/v1/webhooks", {
    method: "POST",
    headers: {
      "Authorization": "Bearer YOUR_API_KEY",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      url: "https://myapp.example.com/hooks/mfoundry",
      events: ["workflow.completed", "project.created"],
      secret: "my-super-secret-value",
    }),
  });
  const webhook = await response.json();
  ```
</CodeGroup>

**Example response (`201 Created`):**

```json theme={null}
{
  "id": "wh_def456",
  "url": "https://myapp.example.com/hooks/mfoundry",
  "events": ["workflow.completed", "project.created"],
  "secret": "my-super-secret-value",
  "active": true,
  "created_at": "2024-06-11T10:30:00Z"
}
```

<Warning>
  The `secret` value is returned **only in this creation response** and is never exposed again through the API. Store it securely in your application's secrets manager immediately. If you lose the secret, delete the webhook and create a new one.
</Warning>

***

## DELETE /v1/webhooks/:id

Delete a webhook subscription. mFoundry immediately stops delivering events to the associated URL. This action cannot be undone. On success, the API returns `204 No Content` with no response body.

<ParamField path="id" type="string" required>
  The unique identifier of the webhook to delete (for example, `wh_abc123`).
</ParamField>

<CodeGroup>
  ```bash cURL theme={null}
  curl --request DELETE \
    --url https://api.mfoundry.io/v1/webhooks/wh_abc123 \
    --header "Authorization: Bearer YOUR_API_KEY"
  ```

  ```javascript JavaScript theme={null}
  await fetch("https://api.mfoundry.io/v1/webhooks/wh_abc123", {
    method: "DELETE",
    headers: { "Authorization": "Bearer YOUR_API_KEY" },
  });
  // Returns 204 No Content — no response body to parse
  ```
</CodeGroup>

***

## Available Events

Subscribe to any combination of the following event types when creating a webhook:

| Event                | Trigger                                       |
| -------------------- | --------------------------------------------- |
| `project.created`    | A new project is created in your organization |
| `project.updated`    | A project's metadata or status is updated     |
| `project.deleted`    | A project is permanently deleted              |
| `workflow.completed` | A workflow run finishes successfully          |
| `workflow.failed`    | A workflow run terminates with an error       |
| `task.assigned`      | A task is assigned to a user                  |

***

## Signature Verification

Every webhook payload mFoundry delivers includes an `X-mFoundry-Signature` header containing an HMAC-SHA256 hex digest, computed using your webhook's `secret` and the raw request body. You should validate this signature before processing any payload to confirm that it originated from mFoundry and has not been tampered with.

For a complete walkthrough of the verification algorithm, see the [Webhooks configuration guide](/configuration/webhooks).

The verification pattern looks like this:

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

  function verifySignature(rawBody, secret, signatureHeader) {
    const expected = crypto
      .createHmac("sha256", secret)
      .update(rawBody, "utf8")
      .digest("hex");

    // Use timingSafeEqual to prevent timing attacks
    const expectedBuf = Buffer.from(expected, "hex");
    const actualBuf = Buffer.from(signatureHeader, "hex");

    if (expectedBuf.length !== actualBuf.length) return false;
    return crypto.timingSafeEqual(expectedBuf, actualBuf);
  }

  // In your request handler:
  const isValid = verifySignature(
    req.rawBody,           // raw, unparsed request body string
    process.env.WEBHOOK_SECRET,
    req.headers["x-mfoundry-signature"]
  );

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

  ```python Python theme={null}
  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)

  # In your request handler:
  is_valid = verify_signature(
      request.get_data(),          # raw bytes of the request body
      os.environ["WEBHOOK_SECRET"],
      request.headers.get("X-mFoundry-Signature", "")
  )

  if not is_valid:
      abort(401, "Invalid signature")
  ```
</CodeGroup>

<Info>
  Always use a constant-time comparison function (such as `crypto.timingSafeEqual` in Node.js or `hmac.compare_digest` in Python) when comparing HMAC signatures. Standard string equality operators are vulnerable to timing attacks that can leak information about the expected value.
</Info>
