Graveinfo.Kobler gravere og infrastruktureiere

Kodeeksempler

Kopier og tilpass eksemplene under. Alle eksempler forutsetter at API-nøkkelen er lagret i miljøvariabelen GRAVEINFO_API_KEY. Nøkkelen opprettes under API-nøkler i dashbordet, og må ha riktige scopes for endepunktene du bruker.

Organisasjonssøk

Søk etter ledningseiere og entreprenører. Krever ikke scope. Se full referanse →

curl

bash
# Søk etter ledningseiere med navn (nøkkel er valgfri, men gir høyere rate limit)
curl "https://graveinfo.no/api/v1/organizations/search?q=Elvia&type=owner" \
  -H "Authorization: Bearer $GRAVEINFO_API_KEY"

# Oppslag på organisasjonsnummer
curl "https://graveinfo.no/api/v1/organizations/search?orgnr=920415523" \
  -H "Authorization: Bearer $GRAVEINFO_API_KEY"

# Filtrer på kommune (0301 = Oslo)
curl "https://graveinfo.no/api/v1/organizations/search?q=vann&municipality=0301&limit=10" \
  -H "Authorization: Bearer $GRAVEINFO_API_KEY"

# Paginering - side 2 med 20 resultater per side
curl "https://graveinfo.no/api/v1/organizations/search?q=nett&limit=20&offset=20" \
  -H "Authorization: Bearer $GRAVEINFO_API_KEY"

JavaScript / TypeScript

Fungerer i Node.js (18+), Deno og i nettleseren. Ingen eksterne avhengigheter. BASE_URL og GRAVEINFO_API_KEY herfra brukes i alle eksemplene under.

typescript
const GRAVEINFO_API_KEY = process.env.GRAVEINFO_API_KEY;
const BASE_URL = 'https://graveinfo.no/api/v1';

async function searchOrganizations(query: string, options?: {
  type?: 'owner' | 'contractor' | 'all';
  municipality?: string;
  limit?: number;
  offset?: number;
}) {
  const params = new URLSearchParams({ q: query });
  if (options?.type) params.set('type', options.type);
  if (options?.municipality) params.set('municipality', options.municipality);
  if (options?.limit) params.set('limit', String(options.limit));
  if (options?.offset) params.set('offset', String(options.offset));

  const response = await fetch(`${BASE_URL}/organizations/search?${params}`, {
    headers: {
      'Authorization': `Bearer ${GRAVEINFO_API_KEY}`,
    },
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(`API error ${response.status}: ${error.error?.message}`);
  }

  return response.json();
}

// Eksempel: søk etter ledningseiere i Oslo
const result = await searchOrganizations('vann', {
  type: 'owner',
  municipality: '0301',
});

console.log(`Fant ${result.meta.total} resultater`);
result.data.forEach((org) => {
  console.log(`${org.name} (${org.orgnr}) - gravemottak: ${org.gravemottak}`);
});

Paginering

Bruk en async generator for å hente alle sider automatisk:

typescript
async function* paginateOrganizations(query: string, pageSize = 20) {
  let offset = 0;
  let total = Infinity;

  while (offset < total) {
    const result = await searchOrganizations(query, { limit: pageSize, offset });
    total = result.meta.total;
    yield* result.data;
    offset += pageSize;
  }
}

// Hent alle treff for "nett"
for await (const org of paginateOrganizations('nett')) {
  console.log(org.name);
}

Python

Krever requests (pip install requests). HEADERS herfra brukes i alle Python-eksemplene under.

python
import os
import requests

GRAVEINFO_API_KEY = os.environ["GRAVEINFO_API_KEY"]
BASE_URL = "https://graveinfo.no/api/v1"
HEADERS = {"Authorization": f"Bearer {GRAVEINFO_API_KEY}"}

def search_organizations(query: str, **kwargs) -> dict:
    """Søk etter organisasjoner i Graveinfo."""
    params = {"q": query, **kwargs}
    response = requests.get(
        f"{BASE_URL}/organizations/search",
        params=params,
        headers=HEADERS,
        timeout=10,
    )
    response.raise_for_status()
    return response.json()


# Eksempel: søk etter ledningseiere i Bergen (4601)
result = search_organizations("vann", type="owner", municipality="4601", limit=10)

print(f"Fant {result['meta']['total']} resultater")
for org in result["data"]:
    print(f"  {org['name']} ({org['orgnr']}) - {org['gravemottak']}")

Paginering

python
def paginate_organizations(query: str, page_size: int = 20):
    """Generator som henter alle sider for et søk."""
    offset = 0
    while True:
        result = search_organizations(query, limit=page_size, offset=offset)
        yield from result["data"]
        offset += page_size
        if offset >= result["meta"]["total"]:
            break

# Hent alle "nett"-organisasjoner
for org in paginate_organizations("nett"):
    print(org["name"])

Feilhåndtering

python
import requests

try:
    result = search_organizations("elvia")
except requests.exceptions.HTTPError as e:
    if e.response.status_code == 429:
        print("Rate limit nådd - vent ett minutt")
    elif e.response.status_code == 401:
        print("Ugyldig API-nøkkel")
    else:
        error = e.response.json()
        print(f"API-feil: {error['error']['message']}")

Infrastrukturdekning per kommune

Finn alle ledningseiere registrert i en kommune. Krever nøkkel, men ikke scope. Se full referanse →

curl

bash
# Finn alle ledningseiere i Oslo (0301)
curl "https://graveinfo.no/api/v1/coverage?municipality=0301" \
  -H "Authorization: Bearer $GRAVEINFO_API_KEY"

# Filtrer på infrastrukturtype
curl "https://graveinfo.no/api/v1/coverage?municipality=0301&type=vann_avlop" \
  -H "Authorization: Bearer $GRAVEINFO_API_KEY"

JavaScript / TypeScript

typescript
async function getCoverage(municipalityNumber: string, type?: string) {
  const params = new URLSearchParams({ municipality: municipalityNumber });
  if (type) params.set('type', type);

  const response = await fetch(
    `${BASE_URL}/coverage?${params}`,
    { headers: { 'Authorization': `Bearer ${GRAVEINFO_API_KEY}` } },
  );

  if (!response.ok) {
    const error = await response.json();
    throw new Error(`API error ${response.status}: ${error.error?.message}`);
  }

  return response.json();
}

// Vis alle eiere i en kommune
const coverage = await getCoverage('0301');
console.log(`${coverage.municipality.name}: ${coverage.total} ledningseiere`);

for (const owner of coverage.owners) {
  const types = owner.infrastructureTypes.join(', ');
  console.log(`  ${owner.name} - ${types} (${owner.gravemottak})`);
}

Python

python
def get_coverage(municipality_number: str, infra_type: str | None = None) -> dict:
    """Hent ledningseiere for en gitt kommune."""
    params: dict = {"municipality": municipality_number}
    if infra_type:
        params["type"] = infra_type
    response = requests.get(
        f"{BASE_URL}/coverage",
        params=params,
        headers=HEADERS,
        timeout=10,
    )
    response.raise_for_status()
    return response.json()


# Bygg en oversikt over flere kommuner
municipalities = ["0301", "4601", "1103"]  # Oslo, Bergen, Stavanger

for mun_num in municipalities:
    coverage = get_coverage(mun_num)
    mun_name = coverage["municipality"]["name"]
    count = coverage["total"]
    print(f"{mun_name} ({mun_num}): {count} ledningseiere")

Opprett og send gravemelding

For entreprenører. Krever scope gravemeldinger:write. Se full referanse → eller steg-for-steg-guiden →

To steg: POST /gravemeldinger oppretter et utkast og returnerer hvilke ledningseiere som ble funnet. POST /gravemeldinger/{id}/send sender det. Sett autoSend: true for å gjøre begge i ett kall. Polygonet er GeoJSON i WGS84 med [lengdegrad, breddegrad]. Eksemplene under bruker en liten firkant i Oslo sentrum.

curl

bash
# 1. Opprett utkast - polygon i WGS84 [lengdegrad, breddegrad]
curl -X POST "https://graveinfo.no/api/v1/gravemeldinger" \
  -H "Authorization: Bearer $GRAVEINFO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Graving Storgata 12",
    "description": "Legging av fiberkabel langs Storgata 12-18",
    "workTypes": ["telekommunikasjon", "grunnarbeid"],
    "location": {
      "type": "Polygon",
      "coordinates": [[
        [10.7522, 59.9139],
        [10.7535, 59.9139],
        [10.7535, 59.9150],
        [10.7522, 59.9150],
        [10.7522, 59.9139]
      ]]
    },
    "plannedStart": "2026-10-01",
    "plannedEnd": "2026-10-14",
    "contactPerson": { "name": "Ola Nordmann", "phone": "91234567" }
  }'
# Svaret har "status": "draft", "referenceNumber", "recipients" og "_links.send"

# 2. Send utkastet (ingen body) - bruk id fra svaret over
curl -X POST "https://graveinfo.no/api/v1/gravemeldinger/3fa85f64-5717-4562-b3fc-2c963f66afa6/send" \
  -H "Authorization: Bearer $GRAVEINFO_API_KEY"

# Alternativ: opprett og send i ett kall med "autoSend": true
curl -X POST "https://graveinfo.no/api/v1/gravemeldinger" \
  -H "Authorization: Bearer $GRAVEINFO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "title": "...", "location": { ... }, "plannedStart": "2026-10-01",
        "plannedEnd": "2026-10-14", "workTypes": ["veiarbeid"],
        "contactPerson": { "name": "Ola Nordmann", "phone": "91234567" },
        "autoSend": true }'

JavaScript / TypeScript

typescript
type WorkType =
  | 'telekommunikasjon' | 'vann_avlop' | 'hoyspenning' | 'lavspenning' | 'veiarbeid'
  | 'grunnarbeid' | 'fjernvarme' | 'annet' | 'pressing' | 'boring';

interface GravemeldingInput {
  title: string;
  location: { type: 'Polygon'; coordinates: number[][][] }; // WGS84 [lng, lat]
  plannedStart: string; // YYYY-MM-DD, i dag eller senere
  plannedEnd: string; // YYYY-MM-DD, samme dag som eller etter plannedStart
  workTypes: WorkType[];
  contactPerson: { name: string; phone: string; email?: string };
  description?: string;
  autoSend?: boolean;
}

async function createGravemelding(input: GravemeldingInput) {
  const response = await fetch(`${BASE_URL}/gravemeldinger`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${GRAVEINFO_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(input),
  });

  if (!response.ok) {
    const error = await response.json();
    // Ved VALIDATION_ERROR ligger feilmelding per felt i error.error.fields
    throw new Error(`API error ${response.status}: ${error.error?.message}`);
  }

  return response.json();
}

async function sendGravemelding(id: string) {
  const response = await fetch(`${BASE_URL}/gravemeldinger/${id}/send`, {
    method: 'POST',
    headers: { 'Authorization': `Bearer ${GRAVEINFO_API_KEY}` },
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(`API error ${response.status}: ${error.error?.message}`);
  }

  return response.json();
}

// 1. Opprett utkast - polygon i WGS84 [lengdegrad, breddegrad]
const draft = await createGravemelding({
  title: 'Graving Storgata 12',
  description: 'Legging av fiberkabel langs Storgata 12-18',
  workTypes: ['telekommunikasjon', 'grunnarbeid'],
  location: {
    type: 'Polygon',
    coordinates: [[
      [10.7522, 59.9139],
      [10.7535, 59.9139],
      [10.7535, 59.9150],
      [10.7522, 59.9150],
      [10.7522, 59.9139],
    ]],
  },
  plannedStart: '2026-10-01',
  plannedEnd: '2026-10-14',
  contactPerson: { name: 'Ola Nordmann', phone: '91234567' },
});

console.log(`Utkast ${draft.referenceNumber} opprettet (status: ${draft.status})`);
console.log(`${draft.recipients.length} ledningseiere funnet i området`);
console.log(`Send med POST ${draft._links.send}`);

if (draft.recipients.length === 0) {
  throw new Error('Ingen ledningseiere i området - /send vil feile med NO_RECIPIENTS_FOUND');
}

// 2. Send utkastet
const sent = await sendGravemelding(draft.id);
console.log(`Sendt ${sent.sentAt} - status: ${sent.status}`);

Med autoSend

Med autoSend: true returneres /send-svaret direkte. Feiler sendingen, får du utkastet tilbake med _warnings:

typescript
// Opprett og send i ett kall
const result = await createGravemelding({ ...input, autoSend: true });

if (result.status === 'sent') {
  console.log(`Sendt! Ref: ${result.referenceNumber}, ${result.recipients.length} mottakere`);
} else if (result._warnings) {
  // Utkastet finnes, men sendingen feilet - kall /send på nytt senere
  console.warn(`Utkast ${result.referenceNumber} lagret, men ikke sendt:`);
  console.warn(result._warnings[0].detail?.error?.code);
}

Python

python
def create_gravemelding(
    title: str,
    work_types: list[str],
    polygon_coords: list[list[list[float]]],
    planned_start: str,
    planned_end: str,
    contact_name: str,
    contact_phone: str,
    description: str | None = None,
    auto_send: bool = False,
) -> dict:
    """Opprett en gravemelding som utkast.

    polygon_coords: lukket ring i WGS84 (EPSG:4326), [lengdegrad, breddegrad].
    """
    payload = {
        "title": title,
        "workTypes": work_types,
        "location": {"type": "Polygon", "coordinates": polygon_coords},
        "plannedStart": planned_start,
        "plannedEnd": planned_end,
        "contactPerson": {"name": contact_name, "phone": contact_phone},
        "autoSend": auto_send,
    }
    if description:
        payload["description"] = description

    response = requests.post(
        f"{BASE_URL}/gravemeldinger",
        json=payload,
        headers=HEADERS,
        timeout=30,
    )
    if response.status_code == 400:
        # VALIDATION_ERROR har feilmelding per felt i error["fields"]
        print(response.json()["error"])
    response.raise_for_status()
    return response.json()


def send_gravemelding(gravemelding_id: str) -> dict:
    """Send et utkast til mottakerne. Ingen body."""
    response = requests.post(
        f"{BASE_URL}/gravemeldinger/{gravemelding_id}/send",
        headers=HEADERS,
        timeout=30,
    )
    response.raise_for_status()
    return response.json()


# 1. Opprett utkast - liten firkant i Oslo sentrum, WGS84 [lng, lat]
coords = [[
    [10.7522, 59.9139],
    [10.7535, 59.9139],
    [10.7535, 59.9150],
    [10.7522, 59.9150],
    [10.7522, 59.9139],
]]

draft = create_gravemelding(
    title="Graving Storgata 12",
    work_types=["telekommunikasjon", "grunnarbeid"],
    polygon_coords=coords,
    planned_start="2026-10-01",
    planned_end="2026-10-14",
    contact_name="Ola Nordmann",
    contact_phone="91234567",
    description="Legging av fiberkabel langs Storgata 12-18",
)

print(f"Utkast {draft['referenceNumber']} opprettet (status: {draft['status']})")
print(f"{len(draft['recipients'])} ledningseiere funnet i området")
print(f"Send med POST {draft['_links']['send']}")

# 2. Send utkastet
if draft["recipients"]:
    sent = send_gravemelding(draft["id"])
    print(f"Sendt {sent['sentAt']} - status: {sent['status']}")
else:
    print("Ingen ledningseiere i området - /send vil feile med NO_RECIPIENTS_FOUND")

List og hent gravemeldinger

Les egne gravemeldinger og sjekk mottakerstatus. Krever scope gravemeldinger:read. Se full referanse →

curl

bash
# List alle gravemeldinger
curl "https://graveinfo.no/api/v1/gravemeldinger" \
  -H "Authorization: Bearer $GRAVEINFO_API_KEY"

# Filtrer på status
curl "https://graveinfo.no/api/v1/gravemeldinger?status=sent" \
  -H "Authorization: Bearer $GRAVEINFO_API_KEY"

# Hent én gravemelding med mottakerstatus og kartblad-lenker
curl "https://graveinfo.no/api/v1/gravemeldinger/3fa85f64-5717-4562-b3fc-2c963f66afa6" \
  -H "Authorization: Bearer $GRAVEINFO_API_KEY"

JavaScript / TypeScript

typescript
async function listGravemeldinger(options?: {
  status?: 'draft' | 'sent' | 'in_progress' | 'completed';
  from?: string;
  to?: string;
  limit?: number;
  offset?: number;
}) {
  const params = new URLSearchParams();
  if (options?.status) params.set('status', options.status);
  if (options?.from) params.set('from', options.from);
  if (options?.to) params.set('to', options.to);
  if (options?.limit) params.set('limit', String(options.limit));
  if (options?.offset) params.set('offset', String(options.offset));

  const url = `${BASE_URL}/gravemeldinger${params.toString() ? '?' + params : ''}`;
  const response = await fetch(url, {
    headers: { 'Authorization': `Bearer ${GRAVEINFO_API_KEY}` },
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(`API error ${response.status}: ${error.error?.message}`);
  }

  return response.json();
}

async function getGravemelding(id: string) {
  const response = await fetch(`${BASE_URL}/gravemeldinger/${id}`, {
    headers: { 'Authorization': `Bearer ${GRAVEINFO_API_KEY}` },
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(`API error ${response.status}: ${error.error?.message}`);
  }

  return response.json();
}

// Hent alle som venter på svar, og vis konflikter og kartblad
const { data } = await listGravemeldinger({ status: 'sent' });
for (const gm of data) {
  console.log(`${gm.referenceNumber} sendt ${gm.sentAt}: ${gm.summary.pending} venter`);
  if (gm.summary.hasConflicts) {
    const detail = await getGravemelding(gm.id);
    const conflicts = detail.recipients.filter((r) => r.response === 'conflict');
    console.log(`  ${conflicts.length} konflikter`);
    for (const r of conflicts) {
      console.log(`  - ${r.name}: ${r.responseNote}`);
      // kartbladUrl er en signert lenke som er gyldig i én time
      if (r.hasKartblad) console.log(`    Kartblad: ${r.kartbladUrl}`);
    }
  }
}

Innboks og svar for ledningseiere

Hent mottatte gravemeldinger og registrer svar. Krever scope innboks:read og innboks:write. Se full referanse →

curl

bash
# Ubesvarte gravemeldinger i innboksen
curl "https://graveinfo.no/api/v1/innboks?status=pending" \
  -H "Authorization: Bearer $GRAVEINFO_API_KEY"

# Svar på en gravemelding - recipientId fra innboksen
curl -X POST "https://graveinfo.no/api/v1/innboks/a1b2c3d4-5678-90ab-cdef-1234567890ab/svar" \
  -H "Authorization: Bearer $GRAVEINFO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "response": "no_conflict", "note": "Ingen ledninger i dette området." }'

JavaScript / TypeScript

typescript
async function getInnboks(status: 'pending' | 'responded' | 'all' = 'pending') {
  const params = new URLSearchParams({ status });
  const response = await fetch(`${BASE_URL}/innboks?${params}`, {
    headers: { 'Authorization': `Bearer ${GRAVEINFO_API_KEY}` },
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(`API error ${response.status}: ${error.error?.message}`);
  }

  return response.json();
}

async function respondToGravemelding(
  recipientId: string,
  response: 'no_conflict' | 'conflict' | 'needs_meeting',
  note?: string,
) {
  const res = await fetch(`${BASE_URL}/innboks/${recipientId}/svar`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${GRAVEINFO_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ response, note }),
  });

  if (!res.ok) {
    const error = await res.json();
    throw new Error(`API error ${res.status}: ${error.error?.message}`);
  }

  return res.json();
}

// Vis ubesvarte gravemeldinger
const { data } = await getInnboks('pending');
for (const item of data) {
  const gm = item.gravemelding;
  console.log(`${gm.referenceNumber}: ${gm.title} (${gm.workTypes.join(', ')})`);
  console.log(`  ${gm.plannedStart} - ${gm.plannedEnd}`);
  console.log(`  Entreprenør: ${gm.contractor?.name}, ${gm.contractor?.phone}`);
  // gm.location er et GeoJSON Polygon i WGS84 - sjekk mot eget ledningsnett her
}

// Svar på den første
if (data.length > 0) {
  const svar = await respondToGravemelding(
    data[0].recipientId,
    'no_conflict',
    'Ingen ledninger i dette området.',
  );
  console.log(`Svar registrert ${svar.respondedAt} (status: ${svar.status})`);
}

Python

python
def get_innboks(status: str = "pending", limit: int = 20, offset: int = 0) -> dict:
    """Hent gravemeldinger organisasjonen er mottaker av."""
    response = requests.get(
        f"{BASE_URL}/innboks",
        params={"status": status, "limit": limit, "offset": offset},
        headers=HEADERS,
        timeout=10,
    )
    response.raise_for_status()
    return response.json()


def respond_to_gravemelding(
    recipient_id: str,
    response: str,  # "no_conflict", "conflict" eller "needs_meeting"
    note: str | None = None,
) -> dict:
    """Registrer svar på en mottatt gravemelding."""
    payload: dict = {"response": response}
    if note:
        payload["note"] = note
    res = requests.post(
        f"{BASE_URL}/innboks/{recipient_id}/svar",
        json=payload,
        headers=HEADERS,
        timeout=10,
    )
    res.raise_for_status()
    return res.json()


# Gå gjennom ubesvarte gravemeldinger
innboks = get_innboks(status="pending")
for item in innboks["data"]:
    gm = item["gravemelding"]
    contractor = gm["contractor"] or {}
    print(f"{gm['referenceNumber']}: {gm['title']} ({', '.join(gm['workTypes'])})")
    print(f"  {gm['plannedStart']} - {gm['plannedEnd']}, {contractor.get('name')}")

    # gm["location"] er et GeoJSON Polygon i WGS84 - sjekk mot eget ledningsnett
    svar = respond_to_gravemelding(
        item["recipientId"],
        "conflict",
        note="Vi har kabler i området. Ta kontakt for påvisning.",
    )
    print(f"  Svar registrert {svar['respondedAt']}")

Sett miljøvariabelen

bash / zshexport GRAVEINFO_API_KEY="gv_live_..."
.env-filGRAVEINFO_API_KEY=gv_live_...
Node.js dotenvrequire("dotenv").config()