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

# Querying

> Filters, sorting, pagination and discovering the model

Every resource is a uniform collection under `https://{clinic}.{region}.practicehub.io/v3/api`:

| Request                  | Result                                                                                  |
| ------------------------ | --------------------------------------------------------------------------------------- |
| `GET /{resource}`        | A page of records matching the filters                                                  |
| `GET /{resource}/{id}`   | One record                                                                              |
| `GET /schema`            | Every resource: attributes, filterable/sortable flags, write operations, accepted input |
| `GET /schema/{resource}` | The same for one resource                                                               |

## Filters

Filter with `?attribute=operator:value`. Any filterable attribute (see `GET /schema/{resource}`) accepts any operator; repeat an attribute to AND several conditions.

```bash theme={null}
# Active patients born in the 1980s, surname starting with "Lo"
curl -g "https://your-clinic.your-region.practicehub.io/v3/api/patients?patient_status=eq:active&dob=between:1980-01-01,1989-12-31&last_name=like:Lo%25" \
  -H "Authorization: Bearer $KEY"

# Pending or arrived appointments in September, newest first
curl -g "https://your-clinic.your-region.practicehub.io/v3/api/appointments?status=in:pending,arrived&start=gte:2026-09-01%2000:00:00&start=lt:2026-10-01%2000:00:00&sort=start:desc" \
  -H "Authorization: Bearer $KEY"
```

| Operator              | Meaning                                                  | Example                               |
| --------------------- | -------------------------------------------------------- | ------------------------------------- |
| `eq`                  | equals (also the default when no operator is given)      | `status=eq:pending`, `status=pending` |
| `ne`                  | not equal                                                | `status=ne:cancelled`                 |
| `gt` `gte` `lt` `lte` | comparison — dates, times and numbers                    | `start=gte:2026-09-01 00:00:00`       |
| `like`                | SQL `LIKE`; `%` is the wildcard (URL-encode it as `%25`) | `email=like:%25@example.com`          |
| `contains`            | Case-insensitive substring — no wildcards to escape      | `last_name=contains:smith`            |
| `in` `not-in`         | comma-separated list                                     | `location_id=in:81,82`                |
| `between`             | inclusive range, exactly two values                      | `dob=between:1980-01-01,1989-12-31`   |
| `null` `not-null`     | is (not) empty                                           | `email=null`, `dob=not-null`          |

Dates and times are in the clinic's timezone, formatted `YYYY-MM-DD` or `YYYY-MM-DD HH:MM:SS`. Attributes the API speaks in words (`patients.sex`: `male`, `female`, `other`) are filtered by those words — `sex=eq:female`, `sex=in:male,female` — never by a stored code; a word outside the set is a `422`.

<Note>
  Filtering on an attribute the resource does not expose, or an unsortable attribute, is a `422` with the attribute named in `errors` — nothing is silently ignored.
</Note>

### Metadata filters

Your own identifiers stored in `metadata` are filterable on every resource with `metadata[key]=operator:value` — see [Metadata](/guides/metadata).

## Sorting

`?sort=attr` sorts ascending; `?sort=attr:desc` descending; `?sort=last_name,first_name:asc` applies one direction to several attributes. `GET /schema/{resource}` marks which attributes are sortable.

## Pagination

Lists are paginated with `page` (from 1) and `page_size` (default and maximum 100). Every list response carries navigation links and counts:

```json theme={null}
{
  "data": [ { "id": 340, "first_name": "Ada", "metadata": {} } ],
  "links": {
    "first": "https://your-clinic.your-region.practicehub.io/v3/api/patients?page=1",
    "prev": null,
    "next": "https://your-clinic.your-region.practicehub.io/v3/api/patients?page=2"
  },
  "meta": { "current_page": 1, "per_page": 100, "total": 1342, "last_page": 14 }
}
```

Follow `links.next` until it is `null`; the links preserve your filters and sort. Ordering is stable — the record id is always applied as a final tiebreaker — so pages never repeat or skip rows for a fixed dataset. Records created or deleted between pages can still shift offset pages, and deep pages get slower on large tables — for syncs and exports use a cursor instead.

### Cursor pagination

Add `cursor=` (empty) to a list request to walk it by cursor. Each page's `links.next` carries an opaque token that encodes where the last row sat in the sort order; the server resumes strictly after it (an index seek, not a skip), so every page costs the same however deep you are, and a record created or deleted elsewhere in the set cannot shift or duplicate what you have already seen. Same filters, sort and `page_size` apply.

```json theme={null}
{
  "data": [ … ],
  "links": { "first": null, "prev": null, "next": "…/patients?cursor=eyJ2IjpbIjIwMjYtMDMtMDEgMTA6MDA6MDAiLDM0MF0sInMiOiIzZjA5In0&sort=created%3Aasc&page_size=100" },
  "meta": { "per_page": 100, "has_more": true }
}
```

Follow `links.next` until it is `null`. Cursors are forward-only, carry no `total`, and are bound to the query they were issued for — reusing one with different filters, sort or `page_size` is a `422`.

## Discovering the model

`GET /schema/{resource}` is the contract for that resource:

```json theme={null}
{
  "entity": "appointments",
  "attributes": {
    "id": { "filterable": true, "sortable": true },
    "start": { "filterable": true, "sortable": true },
    "status": { "filterable": true, "sortable": true }
  },
  "metadata": { "filterable": true, "syntax": "metadata[key]=op:value", "writable": true },
  "write_operations": ["create", "update", "delete"],
  "input": {
    "create": { "patient_id": { "required": true, "rules": ["required", "integer", "min:1"] } },
    "update": { "status": { "required": false, "rules": ["in:pending,arrived,cancelled,missed"] } }
  }
}
```

The [API Reference](/api-reference) is generated from the same source, so both always agree.

## Response shape

* Attributes are `snake_case`; ids are integers; empty values are `null`.
* Every record includes `id` and a `metadata` object.
* Only the attributes listed in `GET /schema/{resource}` are ever returned — new attributes may be added over time, existing ones are not renamed or removed within the current version.

## Deleted records

Records deleted in PracticeHub disappear from their resource. `GET /deleted_entities` lists what was deleted and when, so a sync can remove its copies — filter with `created=gte:{last sync}` and read `entity_type` / `entity_id`.
