Partner-API
Publiser og administrer arrangementene deres i Duito fra deres eget system. REST og JSON, deres egne ID-er, og alt kan sendes på nytt uten å lage duplikater.
https://api.duito.net/partner/v1/openapi.json. Importer den i Postman, Insomnia eller en kodegenerator.Kom i gang
- Registrer organisasjonen på duito.net/partner (se Kom i gang).
- Lag en API-nøkkel i portalen. Den vises bare én gang, så lagre den trygt.
- Send arrangementer som vist under. Før partneren er godkjent, svarer API-et
403 PARTNER_PENDING_APPROVAL.
Vil dere prøve først? Lag en testnøkkel. Den virker med en gang, også før godkjenning, og ingenting dere sender med den blir publisert.
Autentisering
Send nøkkelen i headeren X-Partner-Api-Key på hvert kall. Bruk alltid HTTPS, og hold nøkkelen på serversiden. Den skal aldri ligge i en nettside eller app som brukerne laster ned.
X-Partner-Api-Key: duito_pk_3f9a...
Endepunkter
Alle under https://api.duito.net/partner/v1. {externalId} er deres egen ID, URL-kodet.
| Metode og sti | Gjør |
|---|---|
PUT /events/{externalId} | Oppretter eller oppdaterer. 201 ny, 200 oppdatert. |
POST /events | Det samme, med externalId i body. |
GET /events/{externalId} | Arrangementet slik det ligger i Duito, med status og lenke. |
GET /events | Alle arrangementene deres, nyest oppdatert først. Se liste. |
DELETE /events/{externalId} | Avlyser arrangementet. Se avlysning. |
POST /events/batch | Opptil 100 arrangementer i ett kall. Se batch. |
PUT /events/{externalId}/image | Last opp bildet direkte. Se bilder. |
PUT /series/{seriesId} | Alle datoene i et gjentakende arrangement. Se serier. Også GET og DELETE. |
PUT /venues/{venueId} | Et sted dere bruker igjen og igjen. Se steder og arrangører. Også GET, DELETE og GET /venues. |
PUT /organizers/{organizerId} | En arrangør som vises på arrangementene. Også GET, DELETE og GET /organizers. |
PUT /webhook | Varsler til serveren deres. Se webhooks. |
GET /me | Hvem nøkkelen tilhører, og om det er en testnøkkel. |
Opprett eller oppdater
Kallet er idempotent: første gang opprettes arrangementet, og senere kall med samme ID oppdaterer det. Send hele arrangementet hver gang. Felter dere utelater, nullstilles (for eksempel pris og billettstatus). Unntaket er bildet: utelatt coverImageUrl beholder bildet som er der, og null fjerner det.
Felter
| Felt | Type | Beskrivelse | |
|---|---|---|---|
title | string, 1–200 | påkrevd | Tittel. |
startsAt | ISO 8601 med tidssone | påkrevd | For eksempel 2026-11-14T19:00:00+01:00 eller …Z. |
location | objekt | påkrevd | Én av tre: et registrert sted { "venueId" }, en adresse { "address", "postalCode"?, "city"?, "label"? } som vi slår opp hos Kartverket, eller koordinater { "lat", "lng", "label" }. label er stedsnavnet som vises. |
organizerId | string | valgfri | En registrert arrangør. Navnet vises som arrangør i appen i stedet for deres navn. |
category | string | valgfri | En kategori fra listen. Utelatt: vi velger ut fra tittel og beskrivelse. |
endsAt | ISO 8601 med tidssone | valgfri | Må være etter startsAt. |
description | string, maks 5000 | valgfri | Ren tekst. HTML fjernes. |
price | objekt | valgfri | { "min": 250, "max": 350, "currency": "NOK" } i hele kroner. Utelat max for «fra 250 kr». currency er NOK som standard. |
isFree | boolean | valgfri | true for gratis. Kan ikke kombineres med price. |
ticketStatus | string | valgfri | available, few_left, sold_out eller not_on_sale. Vises i appen. |
status | string | valgfri | scheduled (standard) eller cancelled. |
ticketUrl | URL | valgfri | Lenke til billettsalg eller mer informasjon. |
coverImageUrl | URL | valgfri | Bilde for arrangementet. Vi kopierer det til våre servere, så det fortsatt vises selv om dere senere flytter eller sletter det. |
capacity | heltall > 0 | valgfri | Maks antall deltakere. |
tags | string[], maks 10 | valgfri | Deres egne stikkord, for eksempel ["Jazz", "Konsert"]. |
Eksempel med curl
curl -X PUT https://api.duito.net/partner/v1/events/konsert-2026-11-14 \
-H "X-Partner-Api-Key: duito_pk_..." \
-H "Content-Type: application/json" \
-d '{
"title": "Høstkonsert med byens kammerkor",
"description": "Årets høstkonsert med verk av Grieg og Sommerro.",
"category": "musikk",
"startsAt": "2026-11-14T19:00:00+01:00",
"endsAt": "2026-11-14T21:00:00+01:00",
"location": { "address": "Storgata 1", "postalCode": "0155", "city": "Oslo", "label": "Kulturhuset" },
"price": { "min": 250, "max": 350 },
"ticketStatus": "available",
"ticketUrl": "https://dere.no/billetter/hostkonsert",
"coverImageUrl": "https://dere.no/bilder/hostkonsert.jpg",
"tags": ["Kor", "Klassisk"]
}'
Eksempel i JavaScript
const base = "https://api.duito.net/partner/v1/events";
const headers = {
"X-Partner-Api-Key": process.env.DUITO_API_KEY,
"Content-Type": "application/json",
};
// Opprett eller oppdater - samme kall hver gang noe endres.
const res = await fetch(`${base}/${encodeURIComponent(event.id)}`, {
method: "PUT",
headers,
body: JSON.stringify({
title: event.title,
startsAt: event.start.toISOString(),
location: { address: event.street, city: event.city, label: event.venueName },
price: event.isFree ? undefined : { min: event.lowestPrice },
isFree: event.isFree,
ticketStatus: event.soldOut ? "sold_out" : "available",
ticketUrl: event.ticketUrl,
}),
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${error.code}: ${error.message}`);
}
const saved = await res.json(); // { id, url, status, ... }
// Avlys
await fetch(`${base}/${encodeURIComponent(event.id)}`, { method: "DELETE", headers });
Svar
Alle kall som gjelder ett arrangement, svarer med arrangementet slik det nå ligger i Duito:
{
"externalId": "konsert-2026-11-14",
"id": "5b0c…",
"status": "published",
"url": "https://duito.net/event/5b0c…",
"title": "Høstkonsert med byens kammerkor",
"category": "musikk",
"startsAt": "2026-11-14T18:00:00.000Z",
"endsAt": "2026-11-14T20:00:00.000Z",
"location": { "lat": 59.9127, "lng": 10.7461, "label": "Kulturhuset" },
"coverImageUrl": "https://dere.no/bilder/hostkonsert.jpg",
"price": { "min": 250, "max": 350, "currency": "NOK" },
"isFree": false,
"ticketStatus": "available",
"tags": ["Kor", "Klassisk"],
"createdAt": "2026-10-02T09:12:44.000Z",
"updatedAt": "2026-10-02T09:12:44.000Z"
}
status | Betyr |
|---|---|
published | Synlig i Duito. url er den offentlige siden. |
cancelled | Avlyst. |
merged | Det samme arrangementet er allerede publisert av en annen arrangør eller via en kalenderlenke. Deres billettlenke er lagt til der, og id/url peker dit. Endringer dere sender, brukes ikke på det arrangementet. Kontakt oss hvis det er feil. |
hidden | Skjult av Duito. Kontakt oss hvis dere mener det er feil. |
coverImageUrl er vår kopi av bildet når den er laget (innen en halvtime), ellers lenken dere sendte.
Har vi allerede arrangementet fra en annen nettside (for eksempel et billettsalg vi henter fra), tar dere det over: deres versjon erstatter vår, arrangementet vises med deres navn, og påmeldingene blir med. Svaret er da published, ikke merged.
Avlysning
DELETE /events/{externalId}, eller send arrangementet med "status": "cancelled". Arrangementet vises som avlyst, og de som har meldt seg på får beskjed. Det slettes ikke. Sender dere det inn igjen med scheduled, kommer det tilbake.
Flytter dere et arrangement til en ny dag eller et nytt sted, får de påmeldte også beskjed. Små endringer, som en ny beskrivelse, varsler ikke.
Hente arrangementene deres
GET /partner/v1/events?limit=50&status=published&updatedSince=2026-10-01T00:00:00Z
| Parameter | Beskrivelse |
|---|---|
limit | 1–200, standard 50. |
cursor | nextCursor fra forrige svar, for neste side. |
status | published, cancelled, merged eller hidden. |
updatedSince | Bare arrangementer oppdatert etter dette tidspunktet. |
Svaret er { "events": [...], "nextCursor": "…" }. nextCursor er null på siste side.
Batch
Synkroniserer dere mange arrangementer, send opptil 100 i ett kall. Hvert arrangement valideres og lagres for seg, så én feil stopper ikke resten. Svaret kommer i samme rekkefølge som dere sendte.
Et kall varer høyst rundt 45 sekunder. Arrangementer vi ikke rakk, får feilen BATCH_TIMEOUT. Send bare dem på nytt, det er trygt. Samme externalId to ganger i én batch gir DUPLICATE_IN_BATCH på den siste.
POST https://api.duito.net/partner/v1/events/batch
{ "events": [ { "externalId": "a", "title": "…", … }, { "externalId": "b", … } ] }
200 OK
{
"saved": 1,
"failed": 1,
"results": [
{ "index": 0, "externalId": "a", "result": "created", "event": { … } },
{ "index": 1, "externalId": "b", "result": "error",
"error": { "code": "VALIDATION_ERROR", "message": "startsAt: Invalid ISO datetime" } }
]
}
Serier (gjentakende arrangementer)
Et kurs hver tirsdag eller en forestilling som går i tre uker: send alle datoene i ett kall. Hver dato blir et eget arrangement i Duito, så folk kan melde seg på den dagen de skal.
PUT https://api.duito.net/partner/v1/series/barnekor-host-2026
{
"title": "Barnekor for 6–9 år",
"location": { "venueId": "lillesal" },
"isFree": true,
"dates": [
{ "id": "uke41", "startsAt": "2026-10-06T17:00:00+02:00", "endsAt": "2026-10-06T18:00:00+02:00" },
{ "id": "uke42", "startsAt": "2026-10-13T17:00:00+02:00", "endsAt": "2026-10-13T18:00:00+02:00" },
{ "id": "uke43", "startsAt": "2026-10-20T17:00:00+02:00", "endsAt": "2026-10-20T18:00:00+02:00" }
]
}
Send hele serien på nytt når noe endres. Datoer som er nye, legges til. Datoer som ikke lenger er med, avlyses, og de som har meldt seg på dem får beskjed. GET /series/{seriesId} viser alle datoene, og DELETE avlyser hele serien. Maks 200 datoer.
Flytte en dato: gi hver dato en egen id, som i eksempelet. Sender dere samme id med ny tid, flyttes arrangementet, og de påmeldte får beskjed om ny tid i stedet for at det avlyses. Gi enten alle datoene id, eller ingen. Uten id tolker vi én fjernet og én ny dato i samme kall som en flytting. Endrer dere flere datoer samtidig, avlyses de gamle og de nye legges til.
Steder og arrangører
Bruker dere de samme stedene om og om igjen, registrer dem én gang og vis til dem med venueId:
PUT https://api.duito.net/partner/v1/venues/lillesal
{ "name": "Kulturhuset, lille sal", "address": "Storgata 1", "postalCode": "0155", "city": "Oslo" }
PUT https://api.duito.net/partner/v1/events/konsert-2026-11-14
{ "title": "…", "startsAt": "…", "location": { "venueId": "lillesal" } }
Flytter eller omdøper dere et sted, oppdateres alle kommende arrangementer der, og de påmeldte får beskjed om nytt sted. Svaret sier hvor mange arrangementer som ble oppdatert (eventsUpdated).
Publiserer dere for andre, for eksempel som billettsystem for flere arrangører, registrer arrangørene med PUT /organizers/{organizerId} ({ "name", "url"? }) og send organizerId med arrangementet. Da står det «Arrangert av …» med arrangørens navn i appen.
Bilder
To måter. Send en offentlig lenke i coverImageUrl, så henter vi bildet og lagrer en kopi. Eller last opp selve filen:
curl -X PUT https://api.duito.net/partner/v1/events/konsert-2026-11-14/image \
-H "X-Partner-Api-Key: duito_pk_..." \
-H "Content-Type: image/jpeg" \
--data-binary @plakat.jpg
JPEG, PNG, WebP eller AVIF. Maks 10 MB ved opplasting, 15 MB ved lenke. Vi skalerer bildet, lager en miniatyr og fjerner metadata som GPS-posisjon. Kan vi ikke hente en lenke dere har sendt, fjerner vi bildet heller enn å vise et ødelagt, og sier fra med webhooken event.image_failed.
Testmiljø
Lag en testnøkkel i portalen (kryss av for «Testnøkkel»). Den starter med duito_tk_ og virker mot de samme endepunktene, med disse forskjellene:
- Den virker med en gang, også før partneren er godkjent.
- Ingenting blir publisert. Arrangementene får
"test": trueog"url": null, og vises aldri i appen. - Testarrangementer og ekte arrangementer er helt adskilt. En testnøkkel ser bare testarrangementer, og omvendt.
- Ingen varsler går ut, og testarrangementer slettes etter 7 dager.
Steder, arrangører og webhook er felles for test og ekte. Bytt til en vanlig nøkkel når integrasjonen virker.
Webhooks
Vi sier fra til serveren deres når noe skjer med arrangementene eller kontoen deres som dere ikke gjorde selv:
| Hendelse | Når |
|---|---|
event.hidden | Duito skjulte et arrangement (reason: admin eller reports). |
event.restored | Et skjult arrangement er synlig igjen. |
event.edited | Duito rettet tid, sted eller tittel (changes sier hva). |
event.deleted | Duito slettet et arrangement. |
event.image_failed | Bildelenken kunne ikke hentes (imageUrl, error). |
partner.approved / partner.unapproved | Kontoen ble godkjent, eller satt tilbake til venter. |
partner.deactivated / partner.activated | Kontoen ble deaktivert eller aktivert. |
key.revoked | Duito trakk tilbake en API-nøkkel (name, prefix). |
ping | Når dere tester med POST /webhook/test. |
Sett opp adressen i portalen, eller med API-et:
PUT https://api.duito.net/partner/v1/webhook
{ "url": "https://dere.no/duito-webhook" }
200 OK
{ "url": "https://dere.no/duito-webhook", "active": true, "secret": "whsec_…", … }
Hvert varsel er en JSON-POST med headerne Duito-Event, Duito-Delivery og Duito-Signature. Sjekk signaturen før dere stoler på innholdet, og svar 2xx innen 10 sekunder:
// Node.js. secret = "whsec_…" fra oppsettet, rawBody = body nøyaktig slik den kom.
const crypto = require("node:crypto");
function verifyDuito(rawBody, signatureHeader, secret) {
const parts = Object.fromEntries(signatureHeader.split(",").map((p) => p.split("=")));
const expected = crypto
.createHmac("sha256", secret)
.update(parts.t + "." + rawBody)
.digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300; // maks 5 min gammel
return (
fresh &&
expected.length === parts.v1.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
);
}
Svarer dere ikke 2xx, prøver vi igjen etter 1 minutt, 5 minutter, 30 minutter, 2 timer og 12 timer. GET /webhook/deliveries viser de siste leveransene og hvordan de gikk. Etter 50 feil på rad setter vi webhooken på pause. Lagre den på nytt for å slå den på igjen.
Feil
Alle feil har samme format:
{ "error": { "code": "INVALID_CATEGORY", "message": "Ukjent kategori" } }
| Status | Kode | Betyr |
|---|---|---|
400 | VALIDATION_ERROR | Et felt mangler eller har feil format. Meldingen sier hvilket. |
400 | INVALID_CATEGORY | Kategorien finnes ikke. |
400 | INVALID_CURSOR | Cursor fra nextCursor er ikke gyldig. |
401 | UNAUTHORIZED | Nøkkelen mangler, er feil, eller partneren er deaktivert. |
401 | API_KEY_REVOKED | Nøkkelen er trukket tilbake. Lag en ny i portalen. |
403 | PARTNER_PENDING_APPROVAL | Partneren er ikke godkjent ennå. |
404 | EVENT_NOT_FOUND | Ingen arrangement med den ID-en hos dere. |
404 | SERIES_NOT_FOUND | Ingen serie med den ID-en hos dere. |
422 | GEOCODE_FAILED | Fant ikke adressen. Sjekk den, eller send koordinater. |
422 | UNKNOWN_VENUE / UNKNOWN_ORGANIZER | Stedet eller arrangøren er ikke registrert. Opprett det med PUT først. |
409 | EVENT_MERGED | Arrangementet er slått sammen med et fra en annen kilde, så bildet kan ikke byttes herfra. |
400 | INVALID_IMAGE | Filen er ikke et bilde vi kan lese. |
415 | UNSUPPORTED_MEDIA_TYPE | Bildet er ikke sendt som rå bytes med riktig Content-Type. |
400 | INVALID_WEBHOOK_URL | Webhook-adressen må være en offentlig https://-adresse. |
| i batch | BATCH_TIMEOUT / DUPLICATE_IN_BATCH | Se batch. |
403 | TEST_KEY_NOT_SUPPORTED | Testnøkler virker bare under /partner/v1. |
429 | RATE_LIMITED | For mange kall. Vent litt og prøv igjen. |
Kategorier
Hent gjeldende kategorier fra GET https://api.duito.net/categories (offentlig, ingen nøkkel). Bruk slug-verdien i category. Lista kan utvides, så les den fra endepunktet i stedet for å hardkode den. Per i dag:
| slug | Brukes for |
|---|---|
musikk | Konserter, kor, festivaler med musikk |
sport | Kamper, løp, treninger, turer |
familie | Barn og familie |
sosialt | Quiz, treff, foredrag, markeder |
annet | Alt annet |
Bruk gjerne tags for å beskrive arrangementet mer presist enn kategorien.
Grenser og god praksis
- Maks 100 kall per minutt. Bruk batch for store synkroniseringer.
- Send arrangementet på nytt når noe endres. Det er trygt å sende det samme flere ganger.
- Bruk en stabil ID. Endres den, blir det et nytt arrangement.
- Bildelenker må være offentlig tilgjengelige og peke rett på bildet (JPEG, PNG, WebP eller AVIF, maks 15 MB). Vi fjerner metadata som GPS-posisjon. Vil dere bytte bilde, send en ny lenke: samme lenke som før blir ikke hentet på nytt.
- Bruk
GET /eventsmedupdatedSincefor å kontrollere at alt er kommet fram.
Eldre versjon
POST /partner/events (uten /v1) virker fortsatt for integrasjoner som allerede bruker den. Den tar de samme grunnfeltene (krever category og koordinater) og svarer bare { "status": "created" | "updated" | "attached" | "duplicate" }. Nye integrasjoner bør bruke v1.