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

# Real-time updates

> Keep a local mirror in sync using the updated_after incremental-sync walk.

The Nordic Financial News API supports a polling pattern for keeping a local data mirror in sync. Use the `updated_after` parameter on the `/articles`, `/stories`, and `/calendar_events` endpoints to fetch only records that changed since your last sync.

An `updated_after` request is a resumable walk, not a snapshot. Results are ordered by `updated_at` ascending and cursor-paginated, so you must follow the cursor to the end before you record a new watermark.

<Warning>
  A response is capped at `limit` (default 25, max 100). Advancing your stored timestamp before draining every page skips the remainder permanently.
</Warning>

## How to poll for updates

<Steps>
  <Step title="Request records changed since your watermark">
    Pass `updated_after` with your stored watermark, and request `updated_at` so you can track your position from the data itself:

    ```bash theme={"dark"}
    curl -H "Authorization: Bearer $API_KEY" \
      "https://nordicfinancialnews.com/api/v1/articles?updated_after=2024-01-01T00:00:00Z&fields=id,title,updated_at"
    ```

    Results come back ordered by `updated_at` ascending, cursor-paginated.
  </Step>

  <Step title="Drain the cursor">
    Follow `pagination.next_cursor`, resending the same `updated_after` value alongside it, until `next_cursor` is `null`.

    ```bash theme={"dark"}
    curl -H "Authorization: Bearer $API_KEY" \
      "https://nordicfinancialnews.com/api/v1/articles?updated_after=2024-01-01T00:00:00Z&cursor=eyJpZCI6..."
    ```
  </Step>

  <Step title="Track the highest updated_at you saw">
    As you process records, keep the maximum `updated_at` across every record drained in this walk. That value, not your clock, is your new high-water mark.
  </Step>

  <Step title="Commit the watermark with a one-minute overlap">
    Once the walk finishes, store the highest `updated_at` you saw **minus a one-minute overlap** as your new watermark. Reconcile the re-delivered records by `id` on the next poll.
  </Step>
</Steps>

<Warning>
  The overlap is not optional. A record's `updated_at` is stamped when the write happens, but the record only becomes visible when that write commits a moment later. Without the overlap, a record stamped just below your watermark that commits just after your walk is skipped by this poll and excluded by every later one, because the filter is a strict `updated_at > watermark`. Re-reading a minute of changes is cheap. Losing a record is permanent.
</Warning>

<Tip>
  The `Link` header's `rel="next"` URL preserves your full query string, including `updated_after` and any filters, so following it directly is the safest way to page.
</Tip>

## Polling example

Re-delivery at the overlap margin is expected. Reconcile by `id` so repeated records update in place rather than duplicating.

<CodeGroup>
  ```bash cURL theme={"dark"}
  WATERMARK="2024-01-01T00:00:00Z"

  CURSOR=""
  MAX_SEEN=""
  while :; do
    URL="https://nordicfinancialnews.com/api/v1/articles?updated_after=$WATERMARK&limit=100&fields=id,title,updated_at"
    [ -n "$CURSOR" ] && URL="$URL&cursor=$CURSOR"

    RESPONSE=$(curl -s -H "Authorization: Bearer $API_KEY" "$URL")
    echo "$RESPONSE" | jq -c '.articles[]'   # upsert by id

    # Records arrive in ascending updated_at order, so the last one is the highest.
    LAST=$(echo "$RESPONSE" | jq -r '.articles[-1].updated_at // empty')
    [ -n "$LAST" ] && MAX_SEEN="$LAST"

    CURSOR=$(echo "$RESPONSE" | jq -r '.pagination.next_cursor // empty')
    [ -z "$CURSOR" ] && break
  done

  # Commit the high-water mark minus a one-minute overlap. GNU date shown;
  # on macOS use: date -u -j -f "%Y-%m-%dT%H:%M:%S" "${MAX_SEEN%%.*}" -v-1M +"%Y-%m-%dT%H:%M:%SZ"
  if [ -n "$MAX_SEEN" ]; then
    WATERMARK=$(date -u -d "$MAX_SEEN - 1 minute" +"%Y-%m-%dT%H:%M:%SZ")
  fi
  ```

  ```python Python theme={"dark"}
  import requests
  import time
  from datetime import datetime, timedelta, timezone

  API_KEY = "your_api_key"
  BASE = "https://nordicfinancialnews.com/api/v1"
  headers = {"Authorization": f"Bearer {API_KEY}"}

  OVERLAP = timedelta(minutes=1)
  ENDPOINTS = ["articles", "stories", "calendar_events"]

  # Each endpoint has its own change stream, so each needs its own watermark.
  watermarks = {endpoint: "2024-01-01T00:00:00Z" for endpoint in ENDPOINTS}


  def drain(endpoint, updated_after):
      """Walk every page of changed records. Returns the highest updated_at seen, or None."""
      cursor = None
      high_water = None

      while True:
          params = {"updated_after": updated_after, "limit": 100}
          if cursor:
              params["cursor"] = cursor

          resp = requests.get(f"{BASE}/{endpoint}", headers=headers, params=params)
          resp.raise_for_status()
          body = resp.json()

          for item in body[endpoint]:
              upsert(item)  # reconcile by item["id"]; re-delivery is expected

              seen = datetime.fromisoformat(item["updated_at"].replace("Z", "+00:00"))
              if high_water is None or seen > high_water:
                  high_water = seen

          cursor = body["pagination"]["next_cursor"]
          if cursor is None:
              return high_water


  while True:
      for endpoint in ENDPOINTS:
          high_water = drain(endpoint, watermarks[endpoint])

          # No records changed: keep the existing watermark rather than advancing blindly.
          if high_water is not None:
              watermarks[endpoint] = (high_water - OVERLAP).strftime("%Y-%m-%dT%H:%M:%SZ")

      time.sleep(60)  # poll every 60 seconds
  ```
</CodeGroup>

<Note>
  `updated_after` accepts `YYYY-MM-DD` or `YYYY-MM-DDTHH:MM:SSZ` (optionally with milliseconds). Python's `datetime.isoformat()` emits microseconds and a `+00:00` offset, both of which are rejected with a `400`. Use `strftime("%Y-%m-%dT%H:%M:%SZ")` as shown above.
</Note>

## How the updated\_after parameter works

When `updated_after` is set:

* Results are **ordered by `updated_at` ascending**, with `id` as a tiebreaker, so a partially drained walk can be resumed from its cursor
* Results are **cursor-paginated** and capped at `limit`. Follow `pagination.next_cursor` until it is `null`
* The filter is **strict** (`updated_at > your watermark`), so a record is not redelivered unless it changed again. This is why the one-minute overlap matters: it is what makes redelivery happen at the margin
* The walk covers the **full served dataset**. A first sync from an old watermark returns history, not just recent records
* **ETag caching is disabled** since the results are time-sensitive
* The response includes both newly created and recently modified records

List payloads for articles, stories, and calendar events include `updated_at`. Request it explicitly when using [field projection](/guides/field-projection).

## Removals are not signalled

A record that is withdrawn, hidden, or otherwise falls out of visibility simply stops appearing in walks. There is no tombstone and no deletion event. A mirror built this way is append-and-update, not an exact replica of the served dataset.

## Calendar events

Calendar events sync the same way, at `/api/v1/calendar_events`, with two behaviors specific to the endpoint.

**Sync walks include past events.** The endpoint's default listing returns upcoming events only, floored at today by the issuer's local date. Passing `updated_after` lifts that floor, so a sync walk covers the full served dataset. This matters because the most common calendar change lands on a past event: every event flips from `scheduled` to `published` roughly two days after its date. Corrections and reschedules to past events reach mirrors the same way.

<Note>
  Lifting the floor is forward-looking. Updates to past events that occurred before your currently stored watermark are not replayed. To backfill that history, reset your watermark to an old timestamp and run a fresh sync, which returns full history exactly as it does for articles and stories.
</Note>

**Cancelled events are excluded.** List endpoints return `scheduled` and `published` events and exclude `cancelled` by default, so a `scheduled → cancelled` transition never surfaces in a default sync walk. To track cancellations, poll separately with `status=cancelled`, or use cancellation alerts.

```bash theme={"dark"}
# Track cancellations alongside your main walk
curl -H "Authorization: Bearer $API_KEY" \
  "https://nordicfinancialnews.com/api/v1/calendar_events?updated_after=$WATERMARK&status=cancelled"
```

<Tip>
  A company-level edit such as a rename, visibility change, or listing change re-stamps all of that company's content. One edit can push a company's entire kept event history into a single sync delta. It drains normally via the cursor, and a suddenly large delta is expected rather than a fault.
</Tip>

## Plan coverage

On the free plan, sync is a sliding window rather than a full mirror. Only articles from preview sources are served, within a recency window and up to a count cap, so records leave the window without any signal as newer ones arrive. Building a complete local mirror requires a paid plan. The current free-plan source list, recency window, and caps are listed on the [pricing page](https://nordicfinancialnews.com/pricing).

<Tip>
  Use the `fields` parameter to request only the fields you need, reducing bandwidth and response time. A 30-60 second polling interval works well for most use cases.
</Tip>

## Migrating an existing integration

<Warning>
  Ordering changed: `updated_after` results were previously sorted by `published_at` descending and are now sorted by `updated_at` ascending. If your integration assumed newest-first, update it.

  If you polled `updated_after` before this change, your local copy may have gaps. Re-sync your history once using the drain loop above.
</Warning>
