2026-03-20 19:53:16 +01:00
# jobnet-cli
CLI for the [Jobnet.dk ](https://jobnet.dk ) Danish government job portal API.
**Base URL** : `https://jobnet.dk/bff`
**Authentication** : No credentials required. All public endpoints only need the `x-csrf: 1` request header.
**Format** : All responses are JSON.
---
## Installation
```bash
cd skills/jobnet-search/cli
bun install
```
---
## Commands
| Command | Description |
|---------|-------------|
| `search` | Search for job ads with filters |
| `detail` | Full detail for a single job ad |
| `occupations` | Search occupation types (for building filters) |
| `suggestions` | Typeahead suggestions for job title / keyword search |
All commands accept `--format json|table|plain` (default: `json` ).
All errors are written to **stderr** as `{ "error": "...", "code": "..." }` and the process exits with code `1` .
---
## Regions
| Value | Danish region |
|-------|--------------|
| `HovedstadenOgBornholm` | Hovedstaden og Bornholm |
| `Midtjylland` | Midtjylland |
| `Syddanmark` | Syddanmark |
| `OevrigeSjaelland` | Øvrige Sjælland |
| `Nordjylland` | Nordjylland |
---
## Order types
| Value | Description |
|-------|-------------|
| `PublicationDate` | Newest postings first (default) |
| `BestMatch` | Relevance score (requires `--search-string` ) |
| `ApplicationDate` | Earliest deadline first |
---
## `search` — Search for job ads
**Endpoint** : `GET /FindJob/Search`
```bash
bun run src/cli.ts search [ flags]
```
### Flags
| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `--search-string` | string | — | Free-text keyword search (job title, skills, employer) |
| `--region` | string | — | One region value (see Regions table) |
| `--postal-code` | string | — | Postal code for radius search, e.g. `2100` |
| `--radius` | number | `50` | Radius in km from postal code (requires `--postal-code` ) |
| `--work-hours` | string | — | `FullTime` or `PartTime` |
| `--duration` | string | — | `Permanent` or `Temporary` |
| `--job-type` | string | — | Announcement type: `Ordinaert` , `Efterloenner` , `Foertidspension` |
| `--occupation-area` | string | — | Occupation area identifier, e.g. `10000` |
| `--occupation-group` | string | — | Occupation group identifier, e.g. `10060` |
| `--page` | number | `1` | Page number (1-indexed) |
| `--per-page` | number | `10` | Results per page |
| `--limit` | number | — | Cap total results returned by CLI |
| `--order` | string | `PublicationDate` | Sort order (see Order types table) |
| `--format` | string | `json` | Output format: `json` , `table` , `plain` |
### Example
```bash
bun run src/cli.ts search \
--search-string "sygeplejerske" \
--region HovedstadenOgBornholm \
--work-hours FullTime \
--duration Permanent \
--per-page 5 \
--format table
bun run src/cli.ts search \
--postal-code 8000 \
--radius 25 \
--per-page 10
```
### Response shape
```json
{
"meta" : {
"totalJobAdCount" : 21452 ,
"pageNumber" : 1 ,
"resultsPerPage" : 10 ,
"searchString" : "developer"
},
"facets" : {
"regions" : [
{ "type" : "HovedstadenOgBornholm" , "jobAdCount" : 6642 }
],
"workHours" : [
{ "type" : "FullTime" , "jobAdCount" : 17919 },
{ "type" : "PartTime" , "jobAdCount" : 3533 }
],
"employmentDurations" : [
{ "type" : "Permanent" , "jobAdCount" : 18533 },
{ "type" : "Temporary" , "jobAdCount" : 2919 }
],
"occupationAreas" : [
{ "identifier" : "10000" , "jobAdCount" : 3235 }
],
"countries" : [
{ "label" : "Danmark" , "identifier" : "DK" , "jobAdCount" : 21164 }
]
},
"results" : [
{
"jobAdId" : "9ef43bce-d82b-4ea1-a098-7ff6520f99be" ,
"title" : "Akademisk medarbejder med interesse for arbejdsmiljø og uddannelse" ,
"hiringOrgName" : "Region Midtjylland" ,
"occupation" : "Personalekonsulent" ,
"municipality" : "Viborg" ,
"postalCode" : 8800 ,
"postalDistrictName" : "Viborg" ,
"country" : "Danmark" ,
"publicationDate" : "2026-03-13T00:00:00+01:00" ,
"applicationDeadline" : "2026-04-05T21:59:00+02:00" ,
"applicationDeadlineStatus" : "ExpirationDate" ,
"workHourPartTime" : false ,
"isExternal" : false ,
"hasLogo" : true ,
"logoUrl" : "/bff/SharedComponents/JobAdCard/CompanyLogo/ByJobAdId/9ef43bce-d82b-4ea1-a098-7ff6520f99be" ,
"cvr" : "29190925" ,
"workPlaceAddress" : "" ,
"conceptUriDa" : "http://data.star.dk/esco/occupation/426e017f-ebe5-4bea-b1eb-7d2d5ab3c6db" ,
"isSeen" : false ,
2026-08-19 01:20:23 -05:00
"isFavorite" : false ,
"company" : "Region Midtjylland" ,
"location" : "Viborg" ,
"date" : "2026-03-13" ,
"deadline" : "2026-04-05" ,
"url" : "https://jobnet.dk/find-job/9ef43bce-d82b-4ea1-a098-7ff6520f99be"
2026-03-20 19:53:16 +01:00
}
]
}
```
> **Note**: The `description` field (raw HTML) is intentionally omitted from `search` results for brevity. Use `detail` to retrieve the full job description.
2026-08-19 01:20:23 -05:00
> **Note**: Every result also carries the cross-portal contract fields `company`, `location`, `date`, `deadline` and `url` — derived respectively from `hiringOrgName`, `postalDistrictName`/`municipality`, and the jobnet detail page URL. `/scrape` Step 2 expects search output to include title, company, location, date, and URL, and dates follow the `YYYY-MM-DD` convention of the other portal CLIs. The API's `1900-01-01` deadline sentinel (deadline not disclosed) maps to `null`. The native fields above are preserved unchanged.
2026-03-20 19:53:16 +01:00
> **Note**: `resultsPerPage` and `pageNumber` must always be provided — omitting them while also providing `searchString` causes the API to return error 1014 ("Fejl i formatering af inputs").
---
## `detail` — Full job ad detail
**Endpoint** : `GET /FindJob/JobAdDetails/{id}`
```bash
bun run src/cli.ts detail <id> [ --format json| plain]
```
The `id` is the `jobAdId` UUID from `search` results.
By default the CLI passes `incrementViews=false` to avoid polluting view counts.
### Example
```bash
bun run src/cli.ts detail 9ef43bce-d82b-4ea1-a098-7ff6520f99be
bun run src/cli.ts detail 9ef43bce-d82b-4ea1-a098-7ff6520f99be --format plain
```
### Response shape
```json
{
"id" : "9ef43bce-d82b-4ea1-a098-7ff6520f99be" ,
"title" : "Akademisk medarbejder med interesse for arbejdsmiljø og uddannelse" ,
"body" : "<p>Full HTML job description...</p>" ,
"publicationDateTime" : "2026-03-13T08:06:58.0578635+01:00" ,
"unpublicationDateTime" : "2026-04-05T21:59:00+02:00" ,
"approvalStatus" : "Godkendt" ,
"views" : 2 ,
"createdDateTime" : "2026-03-13T08:06:58.3936145+01:00" ,
"updatedDateTime" : "2026-03-13T08:06:58.3936145+01:00" ,
"isAnonymousEmployer" : false ,
"hasLogo" : true ,
"logoUrl" : "/bff/SharedComponents/JobAdCard/CompanyLogo/ByJobAdId/9ef43bce-d82b-4ea1-a098-7ff6520f99be" ,
"employer" : {
"cvrNumber" : "29190925" ,
"pNumber" : "1003367314" ,
"name" : "Region Midtjylland" ,
"hasCompanyLogo" : true
},
"job" : {
"type" : "Ordinaert" ,
"address" : {
"streetName" : "Heibergs Alle 5A" ,
"city" : "Viborg" ,
"postalCode" : "8800" ,
"municipality" : "Viborg" ,
"countryCode" : "DK" ,
"countryName" : "Danmark"
},
"noFixedWorkplace" : false ,
"isLimitedPeriod" : false ,
"isDisabilityFriendly" : false ,
"isPartTime" : false ,
"employmentDate" : "2026-06-01T00:00:00+02:00" ,
"conceptUriDa" : "http://data.star.dk/esco/occupation/426e017f-ebe5-4bea-b1eb-7d2d5ab3c6db" ,
"preferredLabelDa" : "Personalekonsulent" ,
"driversLicenses" : [],
"classifications" : [],
"shifts" : [],
"isFavorite" : false
},
"application" : {
"deadlineDate" : "2026-04-05T21:59:00+02:00" ,
"availablePositions" : 1 ,
"contactPersons" : [
{
"firstNames" : "Jane" ,
"lastName" : "Doe" ,
"phoneNumber" : "+4512345678"
}
],
"url" : "https://midtjob.dk/ad/..." ,
"urlText" : "" ,
"isApplicationDeadlineASAP" : false
},
"organisationTypeId" : 24 ,
"user" : "Emply 31430747"
}
```
> **Note**: `body` contains raw HTML. In `--format plain` output the CLI strips HTML tags to produce readable text.
> **Note**: `application.url` may be empty for some job ads — the employer may only accept applications through Jobnet's internal system.
---
## `occupations` — Search occupation types
**Endpoint** : `GET /OccupationSearch`
```bash
bun run src/cli.ts occupations --search-string <text> [ flags]
```
Use this command to find occupation identifiers (ESCO concept URIs) to pass as filters to `search` .
### Flags
| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `--search-string` | string | **required** | Search term for occupation, e.g. `sygeplejerske` |
| `--per-page` | number | `10` | Max results to return |
| `--format` | string | `json` | Output format: `json` , `table` , `plain` |
### Example
```bash
bun run src/cli.ts occupations --search-string "sygeplejerske" --per-page 5
```
### Response shape
```json
[
{
"conceptUriDa" : "http://data.star.dk/esco/occupation/56f5d45c-1234-4321-abcd-000000000000" ,
"preferredLabelDa" : "Sygeplejerske" ,
"aliases" : [
{
"aliasIdentifier" : "some-uuid" ,
"conceptUriDa" : "http://data.star.dk/esco/occupation/56f5d45c-1234-4321-abcd-000000000000" ,
"alternativeLabelDa" : "Operationssygeplejerske"
}
]
}
]
```
> **Note**: `conceptUriDa` is the full ESCO concept URI. The UUID portion (last path segment) can be used to build occupation filters for `search`.
---
## `suggestions` — Typeahead suggestions
**Endpoint** : `GET /FindJob/GetTypeaheadSuggestions`
```bash
bun run src/cli.ts suggestions --query <text> [ flags]
```
Returns autocomplete strings for the search box — useful for exploring valid Danish job titles before running a `search` .
### Flags
| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `--query` | string | **required** | Partial search string to complete |
| `--limit` | number | — | Cap number of suggestions returned |
| `--format` | string | `json` | Output format: `json` , `table` , `plain` |
### Example
```bash
bun run src/cli.ts suggestions --query "syge"
bun run src/cli.ts suggestions --query "ingeniør" --limit 5
```
### Response shape
```json
[
"sygepleje" ,
"Sygeplejerske" ,
"Sygeplejerske \"Lægeassistent\""
]
```
> **Note**: Suggestions are tuned for Danish job titles. English terms like "developer" may return an empty array.
---
## Error handling
All errors are written to **stderr** in JSON format and exit with code `1` :
```json
{ "error" : "Job ad not found" , "code" : "NOT_FOUND" }
{ "error" : "API request failed: 500 Internal Server Error" , "code" : "API_ERROR" }
{ "error" : "--query is required" , "code" : "MISSING_REQUIRED" }
{ "error" : "--search-string is required" , "code" : "MISSING_REQUIRED" }
```
---
## URL construction
Job ad detail pages on jobnet.dk:
```
2026-08-19 01:20:23 -05:00
https://jobnet.dk/find-job/{jobAdId}
2026-03-20 19:53:16 +01:00
```
2026-08-19 01:20:23 -05:00
The legacy `https://jobnet.dk/job/{jobAdId}` route redirects anonymous visitors into the
MitID login flow, so it is never emitted. External ads (`isExternal: true` , jobAdIds with an
`E` prefix) 404 on `/find-job/` and hit the login wall on `/job/` - neither route serves them
anonymously; `/find-job/` is still strictly better and external ads are left as-is.
2026-03-20 19:53:16 +01:00
Company logo images (prefix relative logoUrl from API):
```
https://jobnet.dk{logoUrl}
```
Example: `https://jobnet.dk/bff/SharedComponents/JobAdCard/CompanyLogo/ByJobAdId/9ef43bce-d82b-4ea1-a098-7ff6520f99be`
---
## Notes
- All data is from the public `jobnet.dk/bff` REST API — no credentials required.
- The `x-csrf: 1` header must be sent with every request.
- Pagination is 1-indexed (`--page 1` is the first page).
- `search` results intentionally omit the HTML `description` field — use `detail` to fetch it.
- `body` in `detail` responses is raw HTML; use `--format plain` to get stripped text.
- The `occupations` command helps discover ESCO occupation URIs usable as `--occupation-area` / `--occupation-group` seeds for narrowing search results.