API
Offentlig REST-api. GET og HEAD, uten API-nøkkel. CORS er tillatt.
Adresse:
Endepunkter
| Endepunkt | Svar |
|---|---|
GET /api/v1/stores | {"stores": [Store, …]} |
GET /api/v1/prices/{id} | Catalogue |
Kjenner du butikkens id, hent priser direkte. Bruk /api/v1/stores for å finne butikker.
Modeller
Store
| Felt | Type | Innhold |
|---|---|---|
id | integer | Fast offentlig butikkode. |
name | string | Butikknavn. |
street | string | null | Gateadresse. |
postal_code | string | null | Postnummer. |
city | string | null | By. Fylke inkluderes ved like bynavn. |
updated_at | string | null | Dato: YYYY-MM-DD. null når priser mangler. |
verified | boolean | Om siste oppdatering ble bekreftet. |
Catalogue
| Felt | Type | Innhold |
|---|---|---|
updated_at | string | Dato: YYYY-MM-DD. |
verified | boolean | Om siste oppdatering ble bekreftet. |
complete | boolean | Om varelisten er fullstendig. |
products | Product[] | Varer og priser. |
Product
| Felt | Type | Innhold |
|---|---|---|
number | string | Varenummer. |
name | string | Varenavn. |
price | string | null | Pris i NOK, som desimaltekst. null betyr ukjent pris. |
unit | string | null | stk, kg eller l. |
quantity | string | null | Antall enheter prisen gjelder. |
barcodes | string[] | Strekkoder. |
alternatives | Alternative[] | Valgfritt. Kun med når det finnes prisalternativer. |
Alternative
| Felt | Type | Innhold |
|---|---|---|
name | string | Navn. |
price | string | null | Pris i NOK. |
quantity | string | null | Antall, i varens enhet. |
barcodes | string[] | Strekkoder for denne prisen. |
Eksempler
Bytt 606 med ønsket butikkode.
Go
Go 1.22+. go run main.go
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"net/url"
"os"
"time"
)
type Catalogue struct {
UpdatedAt string `json:"updated_at"`
Verified bool `json:"verified"`
Complete bool `json:"complete"`
Products []Product `json:"products"`
}
type Product struct {
Number string `json:"number"`
Name string `json:"name"`
Price *string `json:"price"`
Unit *string `json:"unit"`
Quantity *string `json:"quantity"`
Barcodes []string `json:"barcodes"`
Alternatives []Alternative `json:"alternatives,omitempty"`
}
type Alternative struct {
Name string `json:"name"`
Price *string `json:"price"`
Quantity *string `json:"quantity"`
Barcodes []string `json:"barcodes"`
}
func getJSON(client *http.Client, address string) (Catalogue, error) {
var result Catalogue
response, err := client.Get(address)
if err != nil {
return result, err
}
defer response.Body.Close()
if response.StatusCode == http.StatusTooManyRequests {
return result, fmt.Errorf("HTTP 429: vent %s sekunder", response.Header.Get("Retry-After"))
}
if response.StatusCode != http.StatusOK {
return result, fmt.Errorf("HTTP %d", response.StatusCode)
}
err = json.NewDecoder(response.Body).Decode(&result)
return result, err
}
func run() error {
api, err := url.Parse(__API_URL__)
if err != nil {
return err
}
client := &http.Client{Timeout: 30 * time.Second}
const storeID = 606
data, err := getJSON(client, api.JoinPath("prices", fmt.Sprintf("%d", storeID)).String())
if err != nil {
return err
}
if !data.Verified || !data.Complete {
log.Print("Ubekreftede eller ufullstendige priser.")
}
return json.NewEncoder(os.Stdout).Encode(data.Products)
}
func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}
Python
Python 3.11+. python3 example.py
import json
import sys
from typing import NotRequired, TypedDict, cast
from urllib.error import HTTPError
from urllib.parse import urljoin
from urllib.request import Request, urlopen
class Alternative(TypedDict):
name: str
price: str | None
quantity: str | None
barcodes: list[str]
class Product(TypedDict):
number: str
name: str
price: str | None
unit: str | None
quantity: str | None
barcodes: list[str]
alternatives: NotRequired[list[Alternative]]
class Catalogue(TypedDict):
updated_at: str
verified: bool
complete: bool
products: list[Product]
def get_json(address: str) -> dict[str, object]:
request = Request(address, headers={"Accept": "application/json"})
try:
with urlopen(request, timeout=30) as response:
if response.status != 200:
raise RuntimeError(f"HTTP {response.status}")
return json.load(response)
except HTTPError as error:
with error:
if error.code == 429:
delay = error.headers.get("Retry-After")
raise RuntimeError(f"HTTP 429: vent {delay} sekunder") from error
raise
def main() -> None:
api = __API_URL__
store_id = 606
data = cast(Catalogue, get_json(urljoin(api, f"prices/{store_id}")))
if not data["verified"] or not data["complete"]:
print("Ubekreftede eller ufullstendige priser.", file=sys.stderr)
print(json.dumps(data["products"], ensure_ascii=False))
if __name__ == "__main__":
main()
JavaScript
Node.js 22+. node example.mjs
/**
* @typedef {Object} Alternative
* @property {string} name
* @property {string|null} price
* @property {string|null} quantity
* @property {string[]} barcodes
*
* @typedef {Object} Product
* @property {string} number
* @property {string} name
* @property {string|null} price
* @property {string|null} unit
* @property {string|null} quantity
* @property {string[]} barcodes
* @property {Alternative[]} [alternatives]
*
* @typedef {{updated_at: string, verified: boolean, complete: boolean, products: Product[]}} Catalogue
*/
async function getJSON(address) {
const response = await fetch(address, { signal: AbortSignal.timeout(30_000) });
if (response.status === 429) {
throw new Error(`HTTP 429: vent ${response.headers.get("Retry-After")} sekunder`);
}
if (response.status !== 200) throw new Error(`HTTP ${response.status}`);
return response.json();
}
const api = __API_URL__;
const storeID = 606;
/** @type {Catalogue} */
const data = await getJSON(new URL(`prices/${storeID}`, api));
if (!data.verified || !data.complete) {
console.warn("Ubekreftede eller ufullstendige priser.");
}
console.log(data.products);
Rust
cargo new price-example. Legg koden i src/main.rs og avhengighetene i Cargo.toml. Kjør cargo run.
[dependencies]
reqwest = { version = "0.13", default-features = false, features = ["blocking", "json", "rustls", "gzip"] }
serde = { version = "1", features = ["derive"] }
use reqwest::{StatusCode, Url, blocking::Client, header::RETRY_AFTER};
use serde::Deserialize;
use std::{error::Error, io, time::Duration};
#[derive(Debug, Deserialize)]
pub struct Catalogue {
pub updated_at: String,
pub verified: bool,
pub complete: bool,
pub products: Vec<Product>,
}
#[derive(Debug, Deserialize)]
pub struct Product {
pub number: String,
pub name: String,
pub price: Option<String>,
pub unit: Option<String>,
pub quantity: Option<String>,
pub barcodes: Vec<String>,
#[serde(default)]
pub alternatives: Vec<Alternative>,
}
#[derive(Debug, Deserialize)]
pub struct Alternative {
pub name: String,
pub price: Option<String>,
pub quantity: Option<String>,
pub barcodes: Vec<String>,
}
fn get_json(client: &Client, url: Url) -> Result<Catalogue, Box<dyn Error>> {
let response = client.get(url).send()?;
if response.status() == StatusCode::TOO_MANY_REQUESTS {
let delay = response
.headers()
.get(RETRY_AFTER)
.ok_or("Retry-After mangler")?
.to_str()?;
return Err(io::Error::other(format!("HTTP 429: vent {delay} sekunder")).into());
}
if response.status() != StatusCode::OK {
return Err(io::Error::other(format!("HTTP {}", response.status())).into());
}
Ok(response.json()?)
}
fn main() -> Result<(), Box<dyn Error>> {
let api = Url::parse(__API_URL__)?;
let client = Client::builder().timeout(Duration::from_secs(30)).build()?;
let store_id = 606;
let data = get_json(&client, api.join(&format!("prices/{store_id}"))?)?;
if !data.verified || !data.complete {
eprintln!("Ubekreftede eller ufullstendige priser.");
}
println!("{:#?}", data.products);
Ok(())
}
HTTP-statuskoder
| Status | Betydning |
|---|---|
200 | OK. |
206 | Delvis innhold ved Range-forespørsel. |
304 | Uendret. Bruk lagret svar. |
404 | Ukjent butikk eller priser mangler. |
405 | Kun GET og HEAD støttes. |
416 | Ugyldig byteområde. |
429 | For mange forespørsler. Vent Retry-After sekunder. |
500 | Serverfeil. |
Feilsvar er ren tekst.
Bruksgrenser og cache
120 forespørsler/minutt per IP, med rom for 30 raske forespørsler. GET og HEAD deler grensen.
Begge endepunktene revalideres for å gi siste priser. ETag og gzip støttes; bruk If-None-Match for betingede forespørsler fra serverklienter.