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.

Maskinlesbar spesifikasjon (OpenAPI 3.1): https://api.duito.net/partner/v1/openapi.json. Importer den i Postman, Insomnia eller en kodegenerator.
Eksemplene på denne siden er fiktive. Arrangementer, steder, adresser og organisasjoner som vises i eksemplene (f.eks. «byens kammerkor», «Kulturhuset», «Storgata 1») finnes ikke i Duito. De er illustrative, slik at dere kan se formen på dataene. Erstatt dem med dere selv før dere sender.

Kom i gang

  1. Registrer organisasjonen på duito.net/partner (se Kom i gang).
  2. Lag en API-nøkkel i portalen. Den vises bare én gang, så lagre den trygt.
  3. 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 stiGjør
PUT /events/{externalId}Oppretter eller oppdaterer. 201 ny, 200 oppdatert.
POST /eventsDet samme, med externalId i body.
GET /events/{externalId}Arrangementet slik det ligger i Duito, med status og lenke.
GET /eventsAlle arrangementene deres, nyest oppdatert først. Se liste.
DELETE /events/{externalId}Avlyser arrangementet. Se avlysning.
POST /events/batchOpptil 100 arrangementer i ett kall. Se batch.
PUT /events/{externalId}/imageLast 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 /webhookVarsler til serveren deres. Se webhooks.
GET /meHvem 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

FeltTypeBeskrivelse
titlestring, 1–200påkrevdTittel.
startsAtISO 8601 med tidssonepåkrevdFor eksempel 2026-11-14T19:00:00+01:00 eller …Z.
locationobjektpå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.
organizerIdstringvalgfriEn registrert arrangør. Navnet vises som arrangør i appen i stedet for deres navn.
categorystringvalgfriEn kategori fra listen. Utelatt: vi velger ut fra tittel og beskrivelse.
endsAtISO 8601 med tidssonevalgfriMå være etter startsAt.
descriptionstring, maks 5000valgfriRen tekst. HTML fjernes.
priceobjektvalgfri{ "min": 250, "max": 350, "currency": "NOK" } i hele kroner. Utelat max for «fra 250 kr». currency er NOK som standard.
isFreebooleanvalgfritrue for gratis. Kan ikke kombineres med price.
ticketStatusstringvalgfriavailable, few_left, sold_out eller not_on_sale. Vises i appen.
statusstringvalgfrischeduled (standard) eller cancelled.
ticketUrlURLvalgfriLenke til billettsalg eller mer informasjon.
coverImageUrlURLvalgfriBilde for arrangementet. Vi kopierer det til våre servere, så det fortsatt vises selv om dere senere flytter eller sletter det.
capacityheltall > 0valgfriMaks antall deltakere.
tagsstring[], maks 10valgfriDeres 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"
}
statusBetyr
publishedSynlig i Duito. url er den offentlige siden.
cancelledAvlyst.
mergedDet 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.
hiddenSkjult 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
ParameterBeskrivelse
limit1–200, standard 50.
cursornextCursor fra forrige svar, for neste side.
statuspublished, cancelled, merged eller hidden.
updatedSinceBare 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": true og "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:

HendelseNår
event.hiddenDuito skjulte et arrangement (reason: admin eller reports).
event.restoredEt skjult arrangement er synlig igjen.
event.editedDuito rettet tid, sted eller tittel (changes sier hva).
event.deletedDuito slettet et arrangement.
event.image_failedBildelenken kunne ikke hentes (imageUrl, error).
partner.approved / partner.unapprovedKontoen ble godkjent, eller satt tilbake til venter.
partner.deactivated / partner.activatedKontoen ble deaktivert eller aktivert.
key.revokedDuito trakk tilbake en API-nøkkel (name, prefix).
pingNå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" } }
StatusKodeBetyr
400VALIDATION_ERROREt felt mangler eller har feil format. Meldingen sier hvilket.
400INVALID_CATEGORYKategorien finnes ikke.
400INVALID_CURSORCursor fra nextCursor er ikke gyldig.
401UNAUTHORIZEDNøkkelen mangler, er feil, eller partneren er deaktivert.
401API_KEY_REVOKEDNøkkelen er trukket tilbake. Lag en ny i portalen.
403PARTNER_PENDING_APPROVALPartneren er ikke godkjent ennå.
404EVENT_NOT_FOUNDIngen arrangement med den ID-en hos dere.
404SERIES_NOT_FOUNDIngen serie med den ID-en hos dere.
422GEOCODE_FAILEDFant ikke adressen. Sjekk den, eller send koordinater.
422UNKNOWN_VENUE / UNKNOWN_ORGANIZERStedet eller arrangøren er ikke registrert. Opprett det med PUT først.
409EVENT_MERGEDArrangementet er slått sammen med et fra en annen kilde, så bildet kan ikke byttes herfra.
400INVALID_IMAGEFilen er ikke et bilde vi kan lese.
415UNSUPPORTED_MEDIA_TYPEBildet er ikke sendt som rå bytes med riktig Content-Type.
400INVALID_WEBHOOK_URLWebhook-adressen må være en offentlig https://-adresse.
i batchBATCH_TIMEOUT / DUPLICATE_IN_BATCHSe batch.
403TEST_KEY_NOT_SUPPORTEDTestnøkler virker bare under /partner/v1.
429RATE_LIMITEDFor 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:

slugBrukes for
musikkKonserter, kor, festivaler med musikk
sportKamper, løp, treninger, turer
familieBarn og familie
sosialtQuiz, treff, foredrag, markeder
annetAlt 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 /events med updatedSince for å 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.

Spørsmål om integrasjonen? Kontakt oss, så hjelper vi dere i gang.