Priser hos REMA 1000

Prisoversikt

API

Offentlig REST-api. GET og HEAD, uten API-nøkkel. CORS er tillatt.

Adresse:

Endepunkter

EndepunktSvar
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

FeltTypeInnhold
idintegerFast offentlig butikkode.
namestringButikknavn.
streetstring | nullGateadresse.
postal_codestring | nullPostnummer.
citystring | nullBy. Fylke inkluderes ved like bynavn.
updated_atstring | nullDato: YYYY-MM-DD. null når priser mangler.
verifiedbooleanOm siste oppdatering ble bekreftet.

Catalogue

FeltTypeInnhold
updated_atstringDato: YYYY-MM-DD.
verifiedbooleanOm siste oppdatering ble bekreftet.
completebooleanOm varelisten er fullstendig.
productsProduct[]Varer og priser.

Product

FeltTypeInnhold
numberstringVarenummer.
namestringVarenavn.
pricestring | nullPris i NOK, som desimaltekst. null betyr ukjent pris.
unitstring | nullstk, kg eller l.
quantitystring | nullAntall enheter prisen gjelder.
barcodesstring[]Strekkoder.
alternativesAlternative[]Valgfritt. Kun med når det finnes prisalternativer.

Alternative

FeltTypeInnhold
namestringNavn.
pricestring | nullPris i NOK.
quantitystring | nullAntall, i varens enhet.
barcodesstring[]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

StatusBetydning
200OK.
206Delvis innhold ved Range-forespørsel.
304Uendret. Bruk lagret svar.
404Ukjent butikk eller priser mangler.
405Kun GET og HEAD støttes.
416Ugyldig byteområde.
429For mange forespørsler. Vent Retry-After sekunder.
500Serverfeil.

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.