> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs-unsw-v6.advance-uac.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs-unsw-v6.advance-uac.com/_mcp/server.

# Detecting changes

One endpoint tells you everything of yours that has changed, and what it changed to:

```
GET /institution/applications/changes
```

Each record comes back with its credit decision already attached, so detecting a change and
reading it is one call rather than three.

## The loop

Between polls you keep **one thing**: the cursor.

#### First call only, name a starting point

```
GET /institution/applications/changes?updatedSince=2026-09-01T09:00:00+10:00&size=100
```

Omit `updatedSince` entirely to read your whole history from the beginning.

#### Process the items, keep the cursor

Store `nextCursor` somewhere durable. It is the only state you need.

#### If hasMore is true, call again immediately

```
GET /institution/applications/changes?cursor=<nextCursor>&size=100
```

Repeat until `hasMore` is false. You are then caught up.

#### Later, poll again with the cursor you stored

An empty `items` list means nothing has changed. That is the normal steady state, not an
error — keep your cursor and try again later.

> **Warning**
>
> Sending both `updatedSince` and `cursor` is a `400`. A cursor already carries a position, and
> quietly preferring one would let a client with broken cursor handling look like it works while
> skipping records on every poll.

## What an item contains

```json
{
  "application": { "id": "...", "status": "PROCESSING", "qualifications": [ ... ] },
  "outcome":     { "assessed": true, "finalOutcome": true, "totalCreditUoc": 6, "credits": [ ... ] }
}
```

`application` is exactly what `GET /institution/applications/{id}` returns. `outcome` is exactly
what `GET /institution/applications/{id}/outcome` returns — see
[Credit outcomes](/credit-outcomes) for how to read it.

`outcome` is **`null`** when the application has never been assessed. That is not the same as an
outcome reporting no credit.

## Current state, not history

An application that changes five times appears in five polls, each time carrying its latest
state. There is no event log and no replay of individual changes, so:

* **Deduplicate on `application.id`** and keep the newest copy.
* **You get the new state, not a diff.** Compare against your own copy if you need to know which
  field moved.

There is **no retention limit**. The feed queries live records rather than a log, so any
historical `updatedSince` works. If you ever lose your own copy, replay everything by passing an
old timestamp.

> **Note**
>
> The feed cannot tell you an application was **deleted** — a deleted record simply stops
> appearing. If you need to detect removals, reconcile periodically with a full replay.

## Timestamps must carry an offset

`updatedSince` requires a full ISO-8601 timestamp with an explicit offset. A bare date is
rejected rather than assumed, because the assumption is measurably wrong:

| `updatedSince`              | Records matched |
| --------------------------- | --------------- |
| `2026-08-11T00:00:00+10:00` | 8               |
| `2026-08-11T00:00:00Z`      | 7               |

`Z` is 10am in Sydney, so a caller asking for "the 11th" silently loses a morning and never finds
out. After your first call the cursor handles this for you — which is most of why it is opaque.

## Ordering, and the short delay

Records arrive in the order they changed, and the cursor is a position in that order, so paging
stays correct even while the feed is being written to.

The feed deliberately stops about **30 seconds short of now**. A record's timestamp is taken when
the change is made but only becomes visible when its transaction commits, so a change stamped
slightly earlier can become visible slightly later. Reading right up to the present would let
such a record slip behind an advancing cursor and be **lost permanently** — not delayed, lost.
The window trades a few seconds of latency for not losing records.

So a change made seconds ago will not be in the page you are reading. It will be in the next one.

## A worked poll

```bash
CURSOR=$(cat .cursor 2>/dev/null)
QUERY=${CURSOR:+cursor=$CURSOR}
QUERY=${QUERY:-updatedSince=2026-09-01T00:00:00+10:00}

while : ; do
  BODY=$(curl -s -H "Authorization: Bearer $ACCESS_TOKEN" \
    "$API/institution/applications/changes?$QUERY&size=100")

  echo "$BODY" | jq -c '.items[] | {id: .application.id, uoc: .outcome.totalCreditUoc}'

  NEXT=$(echo "$BODY" | jq -r '.nextCursor // empty')
  [ -n "$NEXT" ] && echo "$NEXT" > .cursor
  [ "$(echo "$BODY" | jq -r .hasMore)" = "true" ] || break
  QUERY="cursor=$NEXT"
done
```

Note the cursor is only overwritten when the response carries one — an empty page returns
`nextCursor: null`, and the position you already hold is still the right one.