Skip to content

Sources and changes

Four endpoints describe the data itself: whether the service is up, which sources feed it, how fresh each is, and what changed lately.

Endpoint Key Purpose
GET /v1/health none Service and database status
GET /v1/sources data Registered sources and their last successful refresh
GET /v1/sources/freshness data The same plus last error and a computed stale flag
GET /v1/changes data Change feed since a date

GET /v1/health

Needs no key. Use it for uptime checks.

curl -s "https://trru.atapinar.com/v1/health"
{
  "data": { "ok": true, "db": true },
  "meta": { "sources": [], "request_id": "a1d21a853e2c48019fdb19db7a411c76", "truncated": false }
}
Field Meaning
data.ok true when the service can read its database.
data.db The database check result (same value as ok today).

When the database is unreachable the endpoint answers 503 with the same body shape and "ok": false, "db": false. Monitor on the status code.

The database check is cached for 10 seconds, for successes and for failures alike. A status can therefore lag the truth by up to 10 seconds, and repeated polling does not create extra database load. Polling more often than every 10 seconds gains nothing. /v1/health checks the service, not the data: use /v1/sources/freshness to see whether the data is current.

GET /v1/sources

Every registered data source with the time of its last successful refresh.

curl -s -H "Authorization: Bearer $TRRU_KEY" "https://trru.atapinar.com/v1/sources"

No parameters. There are 35 sources at the time of writing, ordered by id. The first three rows of the real response (the list is trimmed here):

{
  "data": {
    "rows": [
      {
        "id": "aeroflot",
        "name": "Aeroflot Group: aylık operasyonel sonuçlar (yolcu, doluluk, RPK/ASK)",
        "base_url": "https://ir.aeroflot.com/reporting/traffic-statistics/",
        "schedule": "40 9 8-20 * *",
        "last_ok_at": "2026-11-13T10:52:02.963953+00:00"
      },
      {
        "id": "bis_chpl",
        "name": "Common High Priority Items List (BIS page)",
        "base_url": "https://www.bis.gov/licensing/country-guidance/common-high-priority-items-list-chpl",
        "schedule": "15 6 * * *",
        "last_ok_at": "2026-11-13T06:17:05.185599+00:00"
      },
      {
        "id": "cbr",
        "name": "Bank of Russia (CBR)",
        "base_url": "https://www.cbr.ru",
        "schedule": "30 13 * * 1-6",
        "last_ok_at": "2026-11-13T17:30:30.332163+00:00"
      }
    ],
    "row_count": 35,
    "truncated": false
  },
  "meta": { "sources": [], "request_id": "ba9b36c34b0c4c909b03d0304a6a5855", "truncated": false }
}
Field Meaning
id The source_id used everywhere else: in meta.sources, in /v1/series rows, in /v1/changes.
name Human-readable name. Several are in Turkish.
base_url The publisher's page.
schedule How often the source is refreshed, as a five-field schedule string (minute, hour, day of month, month, day of week). null when a source has none.
last_ok_at When the last successful refresh finished (UTC). null if it never succeeded. This is the fetched_at that appears in meta.sources of data responses.

meta.sources is empty on this endpoint.

GET /v1/sources/freshness

One row per source, answering "is this data current?".

curl -s -H "Authorization: Bearer $TRRU_KEY" "https://trru.atapinar.com/v1/sources/freshness"

First three rows of the real response (the list is trimmed here to three of 35):

{
  "data": {
    "rows": [
      {
        "source_id": "aeroflot",
        "name": "Aeroflot Group: aylık operasyonel sonuçlar (yolcu, doluluk, RPK/ASK)",
        "schedule": "40 9 8-20 * *",
        "last_ok_at": "2026-11-13T10:52:02.963953+00:00",
        "last_error_at": null,
        "last_error_kind": null,
        "newest_data_date": "2026-10-01",
        "expected_interval_hours": 744.0,
        "stale": false
      },
      {
        "source_id": "bis_chpl",
        "name": "Common High Priority Items List (BIS page)",
        "schedule": "15 6 * * *",
        "last_ok_at": "2026-11-13T06:17:05.185599+00:00",
        "last_error_at": null,
        "last_error_kind": null,
        "newest_data_date": null,
        "expected_interval_hours": 24.0,
        "stale": false
      },
      {
        "source_id": "cbr",
        "name": "Bank of Russia (CBR)",
        "schedule": "30 13 * * 1-6",
        "last_ok_at": "2026-11-13T17:30:30.332163+00:00",
        "last_error_at": null,
        "last_error_kind": null,
        "newest_data_date": "2026-11-13",
        "expected_interval_hours": 168.0,
        "stale": false
      }
    ],
    "row_count": 35,
    "truncated": false
  },
  "meta": { "sources": [], "request_id": "1003765268934005a39bdb14c89223fe", "truncated": false }
}
Field Meaning
source_id, name, schedule, last_ok_at As in /v1/sources.
last_error_at Time of the most recent failed or partial refresh, or null.
last_error_kind Coarse class of that failure, or null when the source never failed. Raw error text is not exposed. One of: blocked (the publisher answered 401, 403 or 429), unavailable (server error 5xx, timeout, DNS, TLS or connection problem), format (the page or file could not be parsed, or its layout was unexpected), partial (the run finished but some parts failed), other (anything else).
newest_data_date For sources that supply macro series: the newest observation date across their series. null for sources that do not (sanctions lists, restrictions, news, companies).
expected_interval_hours Expected time between refreshes, estimated from schedule.
stale true when the source has never succeeded, or when last_ok_at is more than 2 times expected_interval_hours ago.

How stale is computed

expected_interval_hours is estimated from the schedule string:

Schedule pattern Expected interval
every N minutes N / 60 hours (news: 0.5)
every N hours N hours
a day of the month is set 744 hours (31 days)
a day of the week is set (a single day or a range) 168 hours (7 days)
anything else (daily) 24 hours
missing or not parseable 24 hours

Then stale = last_ok_at is null OR now - last_ok_at > 2 * expected_interval_hours.

Things to know:

  • The interval is an estimate. A source that runs daily on weekdays only (for example 1-6 as day of week) is judged on a weekly interval, so its stale threshold is 14 days. stale: false is therefore a weaker statement for such sources; compare last_ok_at yourself.
  • stale says the refresh job has not succeeded recently. It does not say the data is up to date: a source can refresh daily while the publisher releases a figure once a quarter. Use newest_data_date (or as_of in the data responses) for the data date.
  • A source can show last_error_at later than last_ok_at and still be stale: false while the last success is recent. A failure that is newer than the last success is worth a look even then.
  • The flag is computed when you ask, from the current time.
body = get("/v1/sources/freshness")
for s in body["data"]["rows"]:
    if s["stale"] or (s["last_error_at"] and s["last_ok_at"] and s["last_error_at"] > s["last_ok_at"]):
        print(s["source_id"], "stale" if s["stale"] else "ok", s["last_ok_at"], s["last_error_at"])

(ISO timestamps in the same format and zone compare correctly as strings.) Check this before you publish a number that looks old. Data coverage and freshness lists what each source covers.

GET /v1/changes

A feed of data changes detected by the refresh jobs, newest first: what the value was, what it is now.

curl -s -H "Authorization: Bearer $TRRU_KEY" \
  "https://trru.atapinar.com/v1/changes?since=2026-11-01&limit=3"
Parameter Type Default Meaning
since string required Start of the window: an ISO date (2026-11-01, read as midnight UTC) or datetime (2026-11-13T12:00:00, or with an offset such as +03:00; a datetime without an offset is UTC). Missing returns 400 invalid or missing parameter(s): since; anything else unparseable (yesterday) returns 400 since must be an ISO date (YYYY-MM-DD) or datetime.
entity string empty Exact entity type, case-sensitive (series). Empty means all entities. An unknown value returns no rows.
limit integer 200 Rows to return. Clamped to 1 to 1000: 0 becomes 1 and 5000 becomes 1000.

Rows are ordered by detected_at, newest first. When more rows match than limit, truncated is true (in data and in meta) and the oldest matching rows are the ones left out. There is no page cursor and no upper bound parameter. To read a long window completely, poll often enough that each call returns fewer rows than limit, keep the highest id you have seen, and skip ids you already hold.

Example response

Request: GET /v1/changes?since=2026-11-01&limit=3

{
  "data": {
    "rows": [
      {
        "id": 727,
        "entity": "series",
        "entity_id": "ru.ruonia",
        "field": "latest",
        "old_value": { "value": 13.92, "obs_date": "2026-11-11" },
        "new_value": { "value": 13.92, "obs_date": "2026-11-12" },
        "source_id": "cbr",
        "as_of": "2026-11-12",
        "detected_at": "2026-11-13T17:30:30.166689+00:00"
      },
      {
        "id": 726,
        "entity": "series",
        "entity_id": "tr.tourism.revenue",
        "field": "latest",
        "old_value": { "value": 3110504.0, "obs_date": "2026-03-01" },
        "new_value": { "value": 8022680.0, "obs_date": "2026-09-01" },
        "source_id": "evds",
        "as_of": "2026-09-01",
        "detected_at": "2026-11-13T17:30:20.875212+00:00"
      },
      {
        "id": 725,
        "entity": "series",
        "entity_id": "tr.aofm",
        "field": "latest",
        "old_value": { "value": 36.0, "obs_date": "2026-11-12" },
        "new_value": { "value": 36.0, "obs_date": "2026-11-13" },
        "source_id": "evds",
        "as_of": "2026-11-13",
        "detected_at": "2026-11-13T17:30:06.177822+00:00"
      }
    ],
    "row_count": 3,
    "truncated": true
  },
  "meta": {
    "sources": [
      { "source_id": "cbr", "fetched_at": "2026-11-13T17:30:30.332163+00:00", "as_of": "2026-11-12" },
      { "source_id": "evds", "fetched_at": "2026-11-13T17:30:29.379365+00:00", "as_of": "2026-11-13" }
    ],
    "request_id": "35670f85ed8045ac8554b7350e868ec7",
    "truncated": true
  }
}

truncated is true here because the window holds more than the 3 requested rows.

Field notes

Field Meaning
id Increasing event id. Newer events have higher ids.
entity Kind of thing that changed.
entity_id Which one: a series id for series.
field Which attribute changed.
old_value, new_value JSON before and after. The shape depends on entity.
source_id Source that produced the change.
as_of Data date of the new value.
detected_at When the refresh job noticed the change (UTC).

Entities in the feed

At the time of writing the feed (which starts on 2026-11-12) has two entities:

entity field Meaning
series latest A series got a new latest observation. entity_id is the series id; old_value and new_value are {value, obs_date}. The value can stay the same while obs_date moves forward (as ru.ruonia above) or the date can jump (monthly series).
parallel_import list The Russian parallel-import list was loaded or changed. entity_id is ru; new_value carries row counts (rows, added, changed, removed) and the reference of the governing order.

Other entity types, such as sanctions list entries, EU annex restrictions and company fields, may be added. entity is an open list: filter on the ones you know and ignore the rest.

The feed is not a complete audit log. It reports changes that the refresh jobs detect; it says nothing about periods before the feed began.

Code

curl -s -H "Authorization: Bearer $TRRU_KEY" \
  "https://trru.atapinar.com/v1/changes?since=2026-11-13&entity=series&limit=50"
import datetime as dt

since = (dt.datetime.now(dt.timezone.utc) - dt.timedelta(hours=24)).strftime("%Y-%m-%dT%H:%M:%S")
body = get("/v1/changes", since=since, entity="series", limit=200)
for c in body["data"]["rows"]:
    print(c["detected_at"], c["entity_id"], c["old_value"], "->", c["new_value"])
if body["meta"]["truncated"]:
    print("more changes exist: raise limit (max 1000) or narrow since")
const since = new Date(Date.now() - 24 * 3600 * 1000).toISOString().slice(0, 19); // UTC, no offset
const body = await get("/v1/changes", { since, entity: "series", limit: "200" });
for (const c of body.data.rows) console.log(c.detected_at, c.entity_id, c.old_value, "->", c.new_value);
if (body.meta.truncated) console.log("more changes exist: raise limit (max 1000) or narrow since");

A daily bulletin can call /v1/changes?since=<yesterday> to list the series that moved, then /v1/latest for their values: see the Daily bulletin guide.