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

# Requests and Responses

> JSON conventions, pagination, team context, errors, and rate limits

The Sitepulse API uses consistent JSON formatting, standard HTTP status codes, and pagination conventions.

## Content type

All requests and responses use JSON:

* **Request bodies:** `application/json`
* **Responses:** `application/json`

## Resource wrapping

### Single resources

Single resources are wrapped in a `data` key:

```json theme={null}
{
  "data": {
    "uuid": "...",
    "name": "Example"
  }
}
```

Some endpoints (for example `GET /me`) nest multiple objects inside `data`:

```json theme={null}
{
  "data": {
    "user": { "name": "...", "email": "..." },
    "teams": [ ... ]
  }
}
```

### Collections

Collections use standard paginator format:

```json theme={null}
{
  "data": [ ... ],
  "links": {
    "first": "...",
    "last": "...",
    "prev": null,
    "next": "..."
  },
  "meta": {
    "current_page": 1,
    "from": 1,
    "last_page": 3,
    "path": "...",
    "per_page": 25,
    "to": 25,
    "total": 62
  }
}
```

<Note>
  Non-paginated collections (for example `GET /teams`) return `data` as an array without `links` / `meta`.
</Note>

## Timestamps

Datetime fields on resource objects (such as `created_at`, `updated_at`, `opened_at`, and `resolved_at`) are ISO 8601 strings in UTC:

```
2026-05-28T14:32:10+00:00
```

<Note>
  Datetime values *inside* check `result` payloads can use tool-specific formats. For example, SSL `valid_from` / `valid_to` are returned as `YYYY-MM-DD HH:MM:SS` without a timezone offset. See [Check results](/api/check-results).
</Note>

## Identifiers

Resources use UUID strings in API paths:

| Resource | Route key | Format      | Example path         |
| -------- | --------- | ----------- | -------------------- |
| Team     | `uuid`    | UUID string | `/teams/{team-uuid}` |
| Site     | `uuid`    | UUID string | `/sites/{uuid}`      |
| Check    | `uuid`    | UUID string | `/checks/{uuid}`     |
| Issue    | `uuid`    | UUID string | `/issues/{uuid}`     |

`{team-uuid}` is only a placeholder label. It is a UUID, named separately in examples so it is not confused with a site, check, or issue UUID.

## Team context

For routes under a team, site, check, or issue, middleware resolves the team and sets it as the token user's current team for that request. This drives plan-limit checks and policies that depend on `currentTeam`.

<Info>
  You do not send a separate `X-Team-Id` header. Scope operations with the team UUID or site uuid in the path.
</Info>

For `GET /issues`, filter to one team with `?team={team-uuid}`. Without `team`, results include all teams where the user has `viewIssues` permission.

## Pagination

List endpoints accept these query parameters:

| Query      | Type        | Default | Max   |
| ---------- | ----------- | ------- | ----- |
| `page`     | integer ≥ 1 | `1`     | -     |
| `per_page` | integer     | `25`    | `100` |

### Example

<CodeGroup>
  ```bash cURL theme={null}
  curl -sS \
    -H "Authorization: Bearer YOUR_TOKEN" \
    -H "Accept: application/json" \
    "https://app.sitepulse.dev/api/v1/teams/{team-uuid}/sites?page=2&per_page=50"
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch(
    'https://app.sitepulse.dev/api/v1/teams/{team-uuid}/sites?page=2&per_page=50',
    {
      headers: {
        Authorization: 'Bearer YOUR_TOKEN',
        Accept: 'application/json',
      },
    }
  );
  const data = await response.json();
  ```

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

  response = requests.get(
      'https://app.sitepulse.dev/api/v1/teams/{team-uuid}/sites',
      headers={
          'Authorization': 'Bearer YOUR_TOKEN',
          'Accept': 'application/json',
      },
      params={'page': 2, 'per_page': 50},
  )
  data = response.json()
  ```

  ```go Go theme={null}
  req, _ := http.NewRequest(
      http.MethodGet,
      "https://app.sitepulse.dev/api/v1/teams/{team-uuid}/sites?page=2&per_page=50",
      nil,
  )
  req.Header.Set("Authorization", "Bearer YOUR_TOKEN")
  req.Header.Set("Accept", "application/json")

  resp, err := http.DefaultClient.Do(req)
  if err != nil {
      panic(err)
  }
  defer resp.Body.Close()

  body, _ := io.ReadAll(resp.Body)
  fmt.Println(string(body))
  ```

  ```ruby Ruby theme={null}
  require 'net/http'
  require 'json'

  uri = URI('https://app.sitepulse.dev/api/v1/teams/{team-uuid}/sites?page=2&per_page=50')
  request = Net::HTTP::Get.new(uri)
  request['Authorization'] = 'Bearer YOUR_TOKEN'
  request['Accept'] = 'application/json'

  response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
    http.request(request)
  end

  data = JSON.parse(response.body)
  ```

  ```php PHP theme={null}
  $ch = curl_init('https://app.sitepulse.dev/api/v1/teams/{team-uuid}/sites?page=2&per_page=50');
  curl_setopt_array($ch, [
      CURLOPT_HTTPHEADER => [
          'Authorization: Bearer YOUR_TOKEN',
          'Accept: application/json',
      ],
      CURLOPT_RETURNTRANSFER => true,
  ]);
  $response = curl_exec($ch);
  curl_close($ch);

  $data = json_decode($response, true);
  ```
</CodeGroup>

## HTTP status codes

| Code  | Meaning                                 |
| ----- | --------------------------------------- |
| `200` | Success                                 |
| `201` | Created (site)                          |
| `202` | Accepted (check run queued)             |
| `204` | Success, no body (delete)               |
| `401` | Missing or invalid token                |
| `402` | Team subscription inactive              |
| `403` | Missing scope or team permission        |
| `404` | Resource not found or not in your teams |
| `422` | Validation error                        |
| `429` | Rate limit exceeded                     |

## Validation errors (422)

Standard validation shape:

```json theme={null}
{
  "message": "The url field is required. (and 1 more error)",
  "errors": {
    "url": ["The url field is required."]
  }
}
```

### Common validation cases

* Duplicate site URL within a team
* Plan site limit reached
* Check tool not allowed on plan or not enabled on site
* Invalid cadence for plan tier
* Invalid filter or sort query parameters

## Authorization vs not found

Cross-team access to sites, checks, and issues returns `404 Not Found` rather than `403`, so resource existence is not leaked across tenants.

## Rate limits

All API routes use the default `api` throttle middleware.

Check runs are further limited:

<Warning>
  `POST /sites/{site}/checks` - **10 requests per minute** per token/user
</Warning>

API check runs also enforce the site's per-tool on-demand cadence, so repeated requests for the same tool may return `422` before the request throttle is reached.

When throttled, the API returns `429 Too Many Requests` with `Retry-After` headers where configured.

## Enums

### Check tools

Tool values in request bodies and responses:

| Value          | Check                  |
| -------------- | ---------------------- |
| `status`       | HTTP uptime            |
| `ssl`          | TLS certificate        |
| `dns`          | DNS records            |
| `broken-links` | Crawl broken links     |
| `performance`  | Lighthouse performance |

### Check status

| Value       | Description                          |
| ----------- | ------------------------------------ |
| `pending`   | Queued or running                    |
| `completed` | Finished successfully                |
| `failed`    | Finished with error                  |
| `skipped`   | Not run (for example plan or config) |

### Issue status

| Value          | Description                   |
| -------------- | ----------------------------- |
| `open`         | Active incident               |
| `acknowledged` | Acknowledged by a team member |
| `resolved`     | Problem has cleared           |

Filter with `status=active` to include both `open` and `acknowledged` issues.

<Note>
  The API supports read access only. Acknowledge and resolve actions are available in the dashboard **Issues** page.
</Note>

### Issue severity

| Value      |
| ---------- |
| `critical` |
| `warning`  |
| `info`     |

## Example requests

<CodeGroup>
  ```bash cURL theme={null}
  # GET request
  curl -sS \
    -H "Authorization: Bearer YOUR_TOKEN" \
    -H "Accept: application/json" \
    "https://app.sitepulse.dev/api/v1/teams/{team-uuid}/sites"

  # POST request
  curl -sS -X POST \
    -H "Authorization: Bearer YOUR_TOKEN" \
    -H "Accept: application/json" \
    -H "Content-Type: application/json" \
    -d '{"url":"https://example.com","name":"Example"}' \
    "https://app.sitepulse.dev/api/v1/teams/{team-uuid}/sites"

  # DELETE request
  curl -sS -X DELETE \
    -H "Authorization: Bearer YOUR_TOKEN" \
    -H "Accept: application/json" \
    "https://app.sitepulse.dev/api/v1/sites/{uuid}"
  ```

  ```javascript JavaScript theme={null}
  const headers = {
    Authorization: 'Bearer YOUR_TOKEN',
    Accept: 'application/json',
  };

  // GET request
  const sites = await fetch('https://app.sitepulse.dev/api/v1/teams/{team-uuid}/sites', {
    headers,
  }).then((response) => response.json());

  // POST request
  const site = await fetch('https://app.sitepulse.dev/api/v1/teams/{team-uuid}/sites', {
    method: 'POST',
    headers: { ...headers, 'Content-Type': 'application/json' },
    body: JSON.stringify({ url: 'https://example.com', name: 'Example' }),
  }).then((response) => response.json());

  // DELETE request
  await fetch('https://app.sitepulse.dev/api/v1/sites/{uuid}', {
    method: 'DELETE',
    headers,
  });
  ```

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

  headers = {
      'Authorization': 'Bearer YOUR_TOKEN',
      'Accept': 'application/json',
  }

  # GET request
  sites = requests.get(
      'https://app.sitepulse.dev/api/v1/teams/{team-uuid}/sites',
      headers=headers,
  ).json()

  # POST request
  site = requests.post(
      'https://app.sitepulse.dev/api/v1/teams/{team-uuid}/sites',
      headers={**headers, 'Content-Type': 'application/json'},
      json={'url': 'https://example.com', 'name': 'Example'},
  ).json()

  # DELETE request
  requests.delete(
      'https://app.sitepulse.dev/api/v1/sites/{uuid}',
      headers=headers,
  )
  ```

  ```go Go theme={null}
  headers := map[string]string{
      "Authorization": "Bearer YOUR_TOKEN",
      "Accept":        "application/json",
  }

  // GET request
  getReq, _ := http.NewRequest(http.MethodGet, "https://app.sitepulse.dev/api/v1/teams/{team-uuid}/sites", nil)
  for key, value := range headers {
      getReq.Header.Set(key, value)
  }
  getResp, _ := http.DefaultClient.Do(getReq)
  defer getResp.Body.Close()

  // POST request
  postBody := strings.NewReader(`{"url":"https://example.com","name":"Example"}`)
  postReq, _ := http.NewRequest(http.MethodPost, "https://app.sitepulse.dev/api/v1/teams/{team-uuid}/sites", postBody)
  for key, value := range headers {
      postReq.Header.Set(key, value)
  }
  postReq.Header.Set("Content-Type", "application/json")
  postResp, _ := http.DefaultClient.Do(postReq)
  defer postResp.Body.Close()

  // DELETE request
  deleteReq, _ := http.NewRequest(http.MethodDelete, "https://app.sitepulse.dev/api/v1/sites/{uuid}", nil)
  for key, value := range headers {
      deleteReq.Header.Set(key, value)
  }
  deleteResp, _ := http.DefaultClient.Do(deleteReq)
  defer deleteResp.Body.Close()
  ```

  ```ruby Ruby theme={null}
  require 'net/http'
  require 'json'

  headers = {
    'Authorization' => 'Bearer YOUR_TOKEN',
    'Accept' => 'application/json',
  }

  # GET request
  get_uri = URI('https://app.sitepulse.dev/api/v1/teams/{team-uuid}/sites')
  get_request = Net::HTTP::Get.new(get_uri)
  headers.each { |key, value| get_request[key] = value }
  sites = Net::HTTP.start(get_uri.hostname, get_uri.port, use_ssl: true) do |http|
    JSON.parse(http.request(get_request).body)
  end

  # POST request
  post_uri = URI('https://app.sitepulse.dev/api/v1/teams/{team-uuid}/sites')
  post_request = Net::HTTP::Post.new(post_uri)
  headers.each { |key, value| post_request[key] = value }
  post_request['Content-Type'] = 'application/json'
  post_request.body = { url: 'https://example.com', name: 'Example' }.to_json
  site = Net::HTTP.start(post_uri.hostname, post_uri.port, use_ssl: true) do |http|
    JSON.parse(http.request(post_request).body)
  end

  # DELETE request
  delete_uri = URI('https://app.sitepulse.dev/api/v1/sites/{uuid}')
  delete_request = Net::HTTP::Delete.new(delete_uri)
  headers.each { |key, value| delete_request[key] = value }
  Net::HTTP.start(delete_uri.hostname, delete_uri.port, use_ssl: true) do |http|
    http.request(delete_request)
  end
  ```

  ```php PHP theme={null}
  $headers = [
      'Authorization: Bearer YOUR_TOKEN',
      'Accept: application/json',
  ];

  // GET request
  $ch = curl_init('https://app.sitepulse.dev/api/v1/teams/{team-uuid}/sites');
  curl_setopt_array($ch, [
      CURLOPT_HTTPHEADER => $headers,
      CURLOPT_RETURNTRANSFER => true,
  ]);
  $sites = json_decode(curl_exec($ch), true);
  curl_close($ch);

  // POST request
  $ch = curl_init('https://app.sitepulse.dev/api/v1/teams/{team-uuid}/sites');
  curl_setopt_array($ch, [
      CURLOPT_POST => true,
      CURLOPT_HTTPHEADER => array_merge($headers, ['Content-Type: application/json']),
      CURLOPT_POSTFIELDS => json_encode(['url' => 'https://example.com', 'name' => 'Example']),
      CURLOPT_RETURNTRANSFER => true,
  ]);
  $site = json_decode(curl_exec($ch), true);
  curl_close($ch);

  // DELETE request
  $ch = curl_init('https://app.sitepulse.dev/api/v1/sites/{uuid}');
  curl_setopt_array($ch, [
      CURLOPT_CUSTOMREQUEST => 'DELETE',
      CURLOPT_HTTPHEADER => $headers,
      CURLOPT_RETURNTRANSFER => true,
  ]);
  curl_exec($ch);
  curl_close($ch);
  ```
</CodeGroup>

## See also

* [Resource reference](/api/resources)
* [Endpoints](/api/endpoints)
