Skip to navigation

Detecting changes

Keeping your records in step without polling applications one at a time
View as Markdown

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.

1

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.

2

Process the items, keep the cursor

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

3

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.

4

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.

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

{
"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 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.

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:

updatedSinceRecords matched
2026-08-11T00:00:00+10:008
2026-08-11T00:00:00Z7

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

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.