# Apino API Changelog

Changes to the public API at `https://api.apino.sk/api/v1`, newest first. Dates
are when the change reached production.

Each entry is one of:

| | Meaning | Do you need to act? |
|---|---|---|
| **Added** | Something new that did not exist before | No. Additions never break a client that parses leniently. |
| **Changed** | Different behaviour from something that already existed | Read it. |
| **Deprecated** | Still works, but is going away on the stated date | Yes, before that date. |
| **Removed** | Gone | Yes, if you have not already. |
| **Fixed** | A defect corrected | Usually no, unless you worked around it. |

Nothing is removed without a `Deprecation` header, a `Sunset` header and at least
six months' notice. See [Compatibility](https://docs.apino.sk) for the full
contract.

---

## 2026-08-31

### Added

- **Financial statements.** `GET /api/v1/organizations/{cin}/financials` lists
  the sets of accounts a Slovak company has filed in the Register účtovných
  závierok — the period, the kind, and when it was filed and prepared.

  The figures are not in it. What this answers is whether a company files and
  how late, which is visible from the periods alone, and a company that has not
  filed for two years is the signal most callers are looking for.

  SK only. A CZ organization returns a validation error rather than an empty
  list, so "we do not cover this" cannot be misread as "files nothing".

- **`financial_statement_missing` in the change feed**, with `source=ruz`. A
  company that has filed accounts before, and has filed none for the following
  period, once even the longest lawful extension has expired. `old_value` is the
  last period filed, `new_value` the one that has not arrived.

  **It reports an absence, not a breach.** The filing deadline follows the tax
  return deadline, and a company may move that by three or six months by
  notifying the tax office — which is private between them and not something we
  can see. So we wait nine months past the end of the period before saying
  anything, and we say only what the register does or does not contain.

  Restricted to companies with a prior filing. It makes no claim about who is
  obliged to file; only that something a company used to do, it has stopped.

  Dated by the deadline rather than by the day we noticed, so it appears once
  and does not repeat.

- **`financial_statement` in the change feed**, with `source=ruz`. A statement
  newly filed for a company you watch reaches `/changes`, your webhooks and the
  digest like any other change, dated by the filing rather than by when we read
  it. `new_value` carries the kind and period, for example `Riadna 2025-01–2025-12`.

  Only genuinely new filings produce an event. The register's changed-since feed
  returns amendments to old statements as readily as new ones — a 2020 statement
  corrected last week comes back in a query for last week — and reporting those
  as filed would be wrong on the word that carries the meaning.

- **Webhooks and the email digest can be narrowed to the change types you want.**
  Set it per endpoint in the dashboard — the same watchlist can drive one
  endpoint that only wants `vat_registration` and another that only wants
  `statutory_bodies`.

  Leaving it empty means everything, which is what every existing endpoint does
  today and will keep doing. Nothing changes unless you narrow it.

  Filtering happens before delivery, so a window containing nothing you asked
  for produces no request at all rather than one with an empty `changes` array.
  Your cursor still advances past it, so narrowing an endpoint does not make it
  re-read the same range every morning.

- **A past delivery can be sent again.** In the dashboard, next to any row in
  the delivery history. Your endpoint being down for an hour used to mean those
  deliveries were gone — the cursor had moved on, and nothing re-reads a window
  it has passed.

  A replay goes out under the **same `event_id`** it had the first time, so a
  receiver that already processed it can drop it. It does not move your cursor
  and it does not count towards the failures that pause an endpoint, so
  retrying against one that is still down costs nothing.

  It carries the change types the endpoint is set to now, not the ones it was
  set to then.

- **`X-Data-Synced-At` on organization data responses.** The completion time of
  the last successful sync of the register the response draws on, as an RFC 3339
  instant:

  ```
  X-Data-Synced-At: 2026-08-31T03:04:12Z
  ```

  It is on `/organizations`, `/organizations/{cin}`, `/organizations/{cin}/detail`,
  `/organizations/batch`, `/verify/{cin}` and `/changes`. A response spanning
  several countries reports the **oldest** of them, because a result is only as
  current as its stalest source.

  The header is absent, not zero, when the value is not known — treat a missing
  header as "no claim" rather than as "stale". It is sent on `304 Not Modified`
  too, and it is not part of the `ETag`, so a re-sync of identical data will not
  invalidate your cache.

- **A public data status page at <https://apino.sk/status>.** When each register
  was last pulled, when the next run is due, and whether any source is behind its
  schedule. It reads the same records the synchronisation runs from, so it cannot
  drift from what actually happened.

  It reports data freshness, not API availability — it runs on the same
  infrastructure, so it could not tell you if that were down.

- **The per-source sync schedule is now documented.** The API description carries
  a table of every upstream source and how often we pull it. Erasures from the
  VAT register (`ds_dphv`) are pulled daily, not weekly with the rest of the
  Financial Administration files; the previous description said otherwise.

- **`GET /changes` can now be polled as a feed.** Two new query parameters turn
  the change history into something a client can synchronise against, instead of
  a list it has to re-read:

  - **`cins`** — up to 100 comma-separated CINs. Poll a whole watchlist in one
    request rather than one request per company.
  - **`since_id`** — return only what was recorded after the last change you saw,
    oldest first. The response then carries **`next_cursor`**: follow it until
    `has_more` is false, store it, and pass it back on your next poll.

  ```
  GET /api/v1/changes?cins=44197519,35757442&since_id=0&source=daily
  ```

  Use `since_id` rather than `offset` for polling. The feed grows at the head, so
  between two polls an offset-paged client is handed rows it already has while
  rows it has not seen are pushed past the end of its window — silently, with no
  error to notice.

  **A cursor walk will not skip a change.** A change is served only once every
  transaction older than it has finished, so one can never turn up behind a
  cursor you have already moved past. The trade is at the head of the feed: a
  change may be held back briefly after it is written, and longer if something
  holds a long transaction open on our side. Nothing is lost — it arrives later.

  We still suggest re-reading a window with `date_from` occasionally and
  de-duplicating on `id`. It costs nothing and covers mistakes neither of us has
  thought of yet.

  One caveat worth knowing: ids increase in the order changes are *recorded*, not
  in the order they happened. A historical backfill records old changes with new
  ids, so they will reach a cursor-paged client. Add `source=daily` if you only
  want what the daily sync detected.

  Nothing about the existing `offset` behaviour changed.

- **Tax events are now in the change feed.** The weekly Financial Administration
  sync is diffed the same way the daily RPO sync is, so two new `change_type`
  values appear in `/changes` and `/organizations/{cin}/timeline`:

  | `change_type` | `old_value` → `new_value` | Means |
  |---|---|---|
  | `vat_registration` | `""` → `registered` | The company registered for VAT |
  | `vat_registration` | `registered` → `deregistered` | It left the VAT payer register |
  | `vat_registration` | `deregistered` → `registered` | It registered again |
  | `tax_reliability` | e.g. `spoľahlivý` → `menej spoľahlivý` | Its tax reliability index moved |

  On a `vat_registration` change, **`new_data` says which of the two ways the
  company left**, because they are not the same news:

  ```json
  {"reason": "erased", "erased_on": "2026-02-11", "violation_year": "2020"}
  ```
  The Financial Administration struck it off for breaching its obligations. This
  is published as a fact, so the change is dated when it happened rather than
  when we noticed — a `sync_date` in the past on one of these is expected.

  ```json
  {"reason": "not_in_register"}
  ```
  It is simply no longer in the current VAT payer register. It left; we are not
  told why, and the field says so rather than implying a reason we do not have.

  These carry a third `source` value, **`tax`**, next to the existing `daily` and
  `historical`. Filter on it with `?source=tax`, or leave it off to get everything.

  A note on timing. We detect these when the Financial Administration
  republishes, and it does not republish everything at the same rate:

  - **Erasures** (`reason: "erased"`) — checked **daily**. Their `sync_date` is
    the date the company was actually struck off, taken from the register, so it
    is often in the past.
  - **Everything else** — VAT registration, ordinary departures, tax reliability —
    is checked **weekly, on Sundays**, and dated when we detected it rather than
    when the decision was made.

  Not every subject in these datasets is in the register — the Financial
  Administration covers entities the RPO does not. Changes we cannot attach to a
  registered organization are dropped rather than guessed at.

### Changed

- **`GET /vat/verify/{identifier}` now reports `is_vat_payer: false` for companies
  that have left the VAT register.** It previously answered `true` for them.

  The VAT payer register lists who is registered *now*; a company that leaves
  simply stops appearing in it, and nothing in the file says so. Until we had a
  way to tell a departure from a short download, we kept serving the last record
  we had — which meant this endpoint, the one you call before deciding whether to
  put VAT on an invoice, could tell you a company was a payer months after it
  stopped being one.

  This is a correction to a value, not a change of shape: `is_vat_payer` has
  always meant "is a VAT payer", and it was returning the wrong answer. Nothing
  about the response's structure changed. But it is worth reading, because for the
  affected companies the answer flips.

  Three fields come with it, all null while a company is still a payer:

  | Field | Meaning |
  |---|---|
  | `deregistered_on` | when it stopped being a payer |
  | `deregistration_reason` | `erased` or `not_in_register` |
  | `violation_year` | for an erasure, the year the grounds arose |

  `erased` means the Financial Administration struck the company off for breaching
  its obligations, and publishes that as a fact — so `deregistered_on` is the real
  date. `not_in_register` means only that the company is no longer in the current
  register: it left, and we are not told why.

  **A company erased years ago that has since registered again is a payer.** These
  three fields stay null for it. A past erasure is history, not current status.

### Added

- **Webhooks.** Changes can now be pushed to you instead of polled. Create a
  watchlist in the dashboard, give it an https endpoint, and we POST the changes
  for its companies once a day. The payload is the same shape `/changes`
  returns, plus the cursor it covered.

  Every delivery is signed: `X-Apino-Signature: t=<unix>,v1=<hmac>`, where the
  HMAC is SHA-256 over `"{t}." + raw_body` with your signing secret. **Verify the
  timestamp as well as the HMAC** — it is inside the signed material so that a
  captured delivery cannot be replayed at you later, and checking only the HMAC
  throws that away. There is a worked example in the API documentation.

  Deliveries are retried, so the same `event_id` can arrive twice; store the ids
  you have handled and ignore repeats. A failed delivery does not advance your
  cursor, so nothing is skipped — the retry carries the identical window. Five
  consecutive failed days pause the endpoint and you resume it from the
  dashboard.

- **`GET /changes/export`** — a day's changes in one download, with no paging.

  ```
  GET /api/v1/changes/export?date=2026-08-30&format=csv
  ```

  This is for importers built around files rather than feeds. `/changes` with
  `since_id` is the better tool if you are writing new code, but "give me
  yesterday in one file" is how a lot of integrations already work, and it is a
  reasonable way to work.

  `ndjson` (default) is one JSON object per line, so it can be processed as it
  arrives instead of being held in memory. `csv` carries the flat columns only.
  `countries` and `source` filter it the same way they filter `/changes`.

  Only completed days can be exported, which means a given date's file never
  changes — ask twice, get the same bytes. For today, use the feed.

  **The day is when we recorded the change, not when it happened.** A tax
  erasure carries the date the register struck the company off, often years
  back, but it lands in the file for the day we first saw it — the day it is
  news to you.

---

### Changed

- **Czech data is now checked daily instead of on the 1st and 16th.** The Czech
  Statistical Office publishes "around the 15th and the end of the month", and on
  17 August it published a day after our check — so that snapshot was not picked
  up for another fortnight. Nothing had failed; the schedule simply missed it.

  Publication frequency at CSU is unchanged, so the data itself still moves about
  twice a month. What changes is the delay between them publishing and us having
  it: at most a day, rather than up to two weeks.

### Fixed

- **`updated_at` on search results was the zero date.** A search by `name` is
  answered from the search index, which did not carry the field, so results came
  back with `"updated_at": "0001-01-01T00:00:00Z"`. The same query answered from
  the database — which happens whenever the index is unavailable — returned the
  real value. Both paths now return the same thing.

  Records indexed before this change report the zero date until the index has
  been rebuilt. Search itself is unaffected throughout: the rebuild replaces
  documents one at a time and never empties the index.

- **`updated_at` is now documented for what it is.** It records when *we* last
  wrote the record, which a sync does for every row it touches and a full
  re-import does for all of them — it is not the date the register last changed
  the company. If that is what you need, `/changes` answers it, and says what
  changed as well as when.

## 2026-08-27

### Added

- **`GET /organizations/{cin}/corrections`** — the defects we found in an
  organization's register record, and what we did about each one. Every applied
  correction carries the register's original text, so the source value stays
  available even though the record we serve is repaired.
  (Aggregate counts across a whole register are an operational signal rather than a
  customer feature, so they stay behind the admin secret for now.)

### Changed

- **Register defects are now repaired before we store a record.** The Slovak RPO
  republishes what ORSR and ŽRSR hold, transcoding errors included. Three classes
  are now fixed on the way in:

  | | Before | After | Scale |
  |---|---|---|---|
  | Decomposed diacritics | `Cˇinnostˇ` | `Činnosť` | 1,607 organizations, 9,209 words |
  | "Unknown date" placeholder | `1800-01-01` | empty | 491,743 organizations, 1,520,647 dates |
  | Non-breaking space lost in transcoding | `v?rozsahu` | `v rozsahu` | 67 organizations, 129 spaces |

  Diacritics are repaired from a reviewed table, not by rule. The characters
  involved also serve as apostrophes in company names — "Prince´s Golf Resort",
  "I´m enjoying life s. r. o.", "FINANCIERE DE L´ARVE" — so a reading is only
  accepted when the repaired word already occurs, thousands of times over, in the
  register's own undamaged text, and a person reviews every reading before it ships.
  Across all 2.28M Slovak records this repairs 9,209 words — almost all of them
  activity descriptions — and changes no company name. Anything it cannot prove is reported rather
  than altered.

  Four more classes are detected but deliberately **not** rewritten, because
  correcting them would mean guessing: damaged tokens with no proven reading
  (2,642 organizations), implausible dates (3,706), letters the register lost to a
  transcoding error (22), and missing postal codes (18,555). They are reported
  through the two endpoints above.

  **What this means for you.** No field changed shape, but values did, in two ways
  worth checking:

  - Dates that held `1800-01-01` are now empty. This reaches 491,743 organizations
    (21.6%), but almost all of it sits on nested records — `validFrom` and
    `validTo` on branches (1,007,647 dates) and shareholders (477,752). A
    company's own `establishment` carried the placeholder in only 470 cases and
    `termination` in 181. Wherever you read a date out of `data`, handle the
    empty case.
  - Text in `activities`, `full_name` and address fields may differ from what you
    received before. Exact-string matching against previously stored values can
    stop matching. Search improved as a result — organizations whose names were
    mangled were previously unfindable.

  Original values remain available at `/organizations/{cin}/corrections`.
