Skip to main content

README.md

# SudregEx

Elixir client for the **Croatian Court Register** (*Sudski registar*) public
open-data REST API served at <https://sudreg-data.gov.hr/>.

The portal exposes machine-readable data about Croatian legal entities (limited
companies, sole traders, institutions, cooperatives, …) — court of jurisdiction,
MBS, OIB, status, name, registered seat, share capital, legal form, activities,
proceedings, financial reports and more. `SudregEx` wraps all 39 v3 "javni"
(public) endpoints, handles OAuth2 auth and token caching, and adds pagination,
snapshot and formatting helpers.

## Installation

Add `:sudreg_ex` to your dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:sudreg_ex, "~> 0.1"}
  ]
end
```

## Getting access

1. Register (free) at <https://sudreg-data.gov.hr/> and verify your e-mail to
   obtain a `client_id` and `client_secret`.

   > The generated credentials **end with two dots** (`..`) — that is part of the
   > credential, not a display artifact. Pass them verbatim.

2. The API uses the **OAuth2 client-credentials flow**; `SudregEx` exchanges the
   credentials for a bearer token (valid 6 hours) and caches it for you.

> **Getting `401 Unauthorized` on every data call?** The token endpoint can issue
> a token *before* your account is fully activated, but the data endpoints stay
> `401` until you **confirm your registration e-mail** — open the verification
> message and click **"Potvrdi"** (Confirm). After that the same credentials and
> token work. (So a working `SudregEx.Auth.fetch_token/1` plus 401s everywhere
> else almost always means an unconfirmed account, not bad credentials.)

A test environment mirrors production at <https://sudreg-data-test.gov.hr/>.

## Configuration

Provide credentials via application config (merged into every
`SudregEx.Client.new/1`):

```elixir
config :sudreg_ex, :client,
  client_id: System.get_env("SUDREG_CLIENT_ID"),
  client_secret: System.get_env("SUDREG_CLIENT_SECRET")
```

A token-cache process starts by default; disable it with
`config :sudreg_ex, start_token_cache: false` and call `SudregEx.Auth.fetch_token/1`
yourself. To point at the test environment, override the URLs on the client:

```elixir
SudregEx.Client.new(
  base_url: "https://sudreg-data-test.gov.hr/api/javni",
  token_url: "https://sudreg-data-test.gov.hr/api/oauth/token"
)
```

## Usage

```elixir
client = SudregEx.Client.new(client_id: "your-id..", client_secret: "your-secret..")

# one record type for all subjects (paginated)
{:ok, %SudregEx.Response{data: rows, total_count: total}} =
  SudregEx.Api.subjekti(client, only_active: true, limit: 50)

# everything about one subject (the API caps this at 6/min — see Throttling)
{:ok, %SudregEx.Response{data: subject}} =
  SudregEx.Api.detalji_subjekta(client, tip_identifikatora: "oib", identifikator: "12345678901")

# code tables (šifrarnici)
{:ok, %SudregEx.Response{data: countries}} = SudregEx.Api.drzave(client)
```

Each endpoint is a function on `SudregEx.Api` taking `(client, opts)`. `opts`
mixes the endpoint's query params (whitelisted per endpoint — an unsupported
param raises `ArgumentError`) with the pipeline opts `:token`, `:token_cache`,
`:format`. Booleans become `"1"`/`"0"`; `nil` params are dropped.

### Pagination

`SudregEx.Api.stream/3` lazily walks a paginated endpoint. Pin a snapshot so the
walk isn't split across the daily refresh:

```elixir
{:ok, snapshot_id} = SudregEx.Api.latest_snapshot_id(client)

SudregEx.Api.stream(client, :tvrtke, snapshot_id: snapshot_id)
|> Stream.map(& &1["ime"])
|> Enum.each(&IO.puts/1)
```

### Throttling `detalji_subjekta`

`detalji_subjekta` is rate-limited to 6 requests/minute for public users. The
optional `SudregEx.RateLimiter` enforces it client-side:

```elixir
{:ok, _} = SudregEx.RateLimiter.start_link(name: :sudreg_limiter)

:ok = SudregEx.RateLimiter.acquire(:sudreg_limiter)
SudregEx.Api.detalji_subjekta(client, tip_identifikatora: "mbs", identifikator: "080000014")
```

### Display helpers

Identifiers come back as numbers without leading zeros; timestamps carry no
timezone:

```elixir
SudregEx.Format.format_mbs(80_000_014)        #=> "080000014"
SudregEx.Format.format_oib(12_345_678_901)    #=> "12345678901"
SudregEx.Format.parse_timestamp("2026-06-15T00:00:00")
#=> {:ok, ~N[2026-06-15 00:00:00]}
```

### Results and errors

Success is `{:ok, %SudregEx.Response{}}` — `data` (the decoded rows) plus metadata
harvested from response headers (`snapshot_id`, `total_count`, `rows_returned`,
`log_id`, `seconds_elapsed`, `timestamp`). Failure is
`{:error, %SudregEx.Error{}}` carrying `http_status` and, for API JSON errors,
`error_code` / `error_message` / `log_id` (quote `log_id` when contacting support).

## Key API concepts

* **Identifiers** — subjects are keyed by **MBS** (9-digit court register number)
  and **OIB** (11-digit tax number), returned as numbers *without* leading zeros
  (use `SudregEx.Format`).
* **Snapshots** — data refreshes once per working day; each refresh is a numbered
  snapshot. Pass `snapshot_id:` to keep multi-call reads consistent.
* **Two usage modes** — `detalji_subjekta` returns *everything* about one subject
  in one call (rate-limited, not for bulk); the per-table endpoints return one
  kind of record for *all* subjects (for building a local replica via `stream/3`).
* **Historisation** — rows carry `status` (1 active, 0 historical, 5 deleted,
  8/9 inconsistent) and `prbu_od`/`prbu_do` (entry ordinals). Pass
  `only_active: true` for just the currently-valid rows.

## Reference

The OpenAPI 3.0 spec for the public service ships with the package at
[`priv/doc/open_api_javni_v3.json`](priv/doc/open_api_javni_v3.json) (39
endpoints). The official developer guide (Croatian PDF) is committed in the
repository at `priv/doc/upute-za-razvojne-inzenjere-v3.0.0.pdf` and available on
the [portal](https://sudreg-data.gov.hr/).

## License

MIT — see [LICENSE](https://gitlab.com/banianitc/sudreg_ex/-/blob/master/LICENSE).