> ## Documentation Index
> Fetch the complete documentation index at: https://corsair-managed-webhooks.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Hub REST API

> Integrate Corsair Hub from any language over plain HTTP — no SDK required.

The Corsair SDK is TypeScript-only, but Hub itself is a plain HTTP service. A Go, Python, or Ruby backend integrates by calling these endpoints directly. Credentials are still delivered to **your** endpoint and stored in **your** database — Hub stores none.

Every request authenticates with your project API key:

```http theme={null}
Authorization: Bearer ck_dev_...
```

## Create a connect session

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://auth.corsair.dev/connect/sessions \
    -H "Authorization: Bearer $CORSAIR_DEV_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "tenantId": "user_123",
      "deliveryUrl": "https://yourapp.com/api/corsair",
      "plugins": [{ "plugin": "github", "oauthMode": "managed" }]
    }'
  ```

  ```ts Node theme={null}
  const res = await fetch("https://auth.corsair.dev/connect/sessions", {
      method: "POST",
      headers: {
          Authorization: `Bearer ${process.env.CORSAIR_DEV_API_KEY}`,
          "Content-Type": "application/json",
      },
      body: JSON.stringify({
          tenantId: "user_123",
          deliveryUrl: "https://yourapp.com/api/corsair",
          plugins: [{ plugin: "github", oauthMode: "managed" }],
      }),
  });
  const { connectUrl } = await res.json();
  ```

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

  res = requests.post(
      "https://auth.corsair.dev/connect/sessions",
      headers={"Authorization": f"Bearer {os.environ['CORSAIR_DEV_API_KEY']}"},
      json={
          "tenantId": "user_123",
          "deliveryUrl": "https://yourapp.com/api/corsair",
          "plugins": [{"plugin": "github", "oauthMode": "managed"}],
      },
  )
  connect_url = res.json()["connectUrl"]
  ```

  ```go Go theme={null}
  payload, _ := json.Marshal(map[string]any{
      "tenantId":    "user_123",
      "deliveryUrl": "https://yourapp.com/api/corsair",
      "plugins":     []map[string]string{{"plugin": "github", "oauthMode": "managed"}},
  })
  req, _ := http.NewRequest("POST", "https://auth.corsair.dev/connect/sessions", bytes.NewReader(payload))
  req.Header.Set("Authorization", "Bearer "+os.Getenv("CORSAIR_DEV_API_KEY"))
  req.Header.Set("Content-Type", "application/json")
  res, _ := http.DefaultClient.Do(req)
  ```
</CodeGroup>

Returns `{ "connectUrl", "token", "projectId", "expiresAt" }`. Redirect the user's browser to `connectUrl`. Hub hosts the connect page and the OAuth callback.

## Receive the delivery

When the user finishes connecting, Hub POSTs a signed JSON envelope to your `deliveryUrl`. The body is `{ "type", "payload" }`, with these headers:

| Header                | Value                                                                                    |
| --------------------- | ---------------------------------------------------------------------------------------- |
| `x-corsair-signature` | `sha256=<hex>` — HMAC-SHA256 of the **raw request body**, keyed with your signing secret |
| `x-corsair-timestamp` | Unix seconds when Hub sent it; reject if older than a few minutes (replay guard)         |
| `x-corsair-project`   | Your project id                                                                          |
| `x-corsair-nonce`     | Unique per delivery                                                                      |

Verify before trusting the body — recompute the HMAC over the raw bytes and compare in constant time:

<CodeGroup>
  ```python Python theme={null}
  import hashlib, hmac, time

  def verify(raw_body: bytes, headers, signing_secret: str) -> bool:
      sig = headers["x-corsair-signature"].removeprefix("sha256=")
      ts = int(headers["x-corsair-timestamp"])
      if abs(time.time() - ts) > 300:            # reject stale deliveries
          return False
      expected = hmac.new(signing_secret.encode(), raw_body, hashlib.sha256).hexdigest()
      return hmac.compare_digest(sig, expected)   # constant-time compare
  ```

  ```ts Node theme={null}
  import { createHmac, timingSafeEqual } from "node:crypto";

  function verify(rawBody: Buffer, headers: Record<string, string>, signingSecret: string): boolean {
      const sig = headers["x-corsair-signature"].replace(/^sha256=/, "");
      const ts = parseInt(headers["x-corsair-timestamp"] ?? "0", 10);
      if (isNaN(ts) || Math.abs(Date.now() / 1000 - ts) > 300) return false; // reject stale or malformed
      const expected = createHmac("sha256", signingSecret).update(rawBody).digest("hex");
      const a = Buffer.from(sig), b = Buffer.from(expected);
      return a.length === b.length && timingSafeEqual(a, b); // constant-time
  }
  ```

  ```go Go theme={null}
  func verify(rawBody []byte, headers http.Header, signingSecret string) bool {
      sig := strings.TrimPrefix(headers.Get("x-corsair-signature"), "sha256=")
      ts, _ := strconv.ParseInt(headers.Get("x-corsair-timestamp"), 10, 64)
      if math.Abs(float64(time.Now().Unix()-ts)) > 300 { // reject stale
          return false
      }
      mac := hmac.New(sha256.New, []byte(signingSecret))
      mac.Write(rawBody)
      expected := hex.EncodeToString(mac.Sum(nil))
      return hmac.Equal([]byte(sig), []byte(expected)) // constant-time
  }
  ```
</CodeGroup>

Only after `verify` passes: parse the body, exchange or store the credential, and respond `200`.

## List connections

```bash cURL theme={null}
curl https://auth.corsair.dev/projects/{projectId}/connections \
  -H "Authorization: Bearer $CORSAIR_DEV_API_KEY"
```

Returns an array of `{ tenantId, plugin, status, authKind, connectedAt, expiresAt }`, deduplicated by `tenantId:plugin`.

## Rate limits

Connect and permission session creation share a limit of **100 sessions per hour per project**. Over the limit returns HTTP 429. Malformed requests do not consume quota.
