Vipps · faste trekk · TypeScript

Vipps Recurring uten omvei

Faste trekk med Vipps får du ikke gjennom en betalingsformidler — verken Stripe, Mollie eller Adyen videreselger dem. Du snakker med API-et selv. Fire mønstre skiller en integrasjon som holder fra en som dobbelttrekker kunder ved månedsslutt. Her er alle fire, og hele koden, MIT-lisensiert.

Asbjørn Riddervold28. august 2026Bygger betalings- og plattformløsninger. Denne teksten kommer ut av en Vipps-integrasjon jeg har kjørt i produksjon.

Kartet, kort fortalt

Engangsbetaling med Vipps er løst omtrent overalt: det finnes offisielle, gratis plugins for WooCommerce, Magento, Shopify, Wix og flere, og de dekker der volumet av norske nettbutikker faktisk ligger.

Faste trekk er en annen historie. Mollies dokumentasjon svarer «Recurring: No». Stripe behandler Vipps som en engangslommebok og er fortsatt i lukket forhåndsvisning. Vipps peker selv utviklere mot API-et framfor Node-biblioteket sitt, som ikke lenger vedlikeholdes aktivt. Og etter at Checkout ble solgt til Kustom, er ePayment og Recurring flatene Vipps satser videre på — bygger du på Recurring nå, bygger du på noe de beholder.

Summen er ikke at noe er ødelagt. Summen er at faste trekk med Vipps er en norsk kapabilitet uten mellomledd, og at du derfor må kunne mekanikken selv. API-dokumentasjonen til Vipps er blant de bedre jeg har jobbet mot — idempotensregelen står der svart på hvitt. Det som mangler er ikke dokumentasjon. Det er kode.

Mønster 1: tre objekter, ikke ett

Den vanligste feilantakelsen kommer fra folk som har brukt Stripe Subscriptions: at avtalen trekker penger. Det gjør den ikke. Avtalen er kundens samtykke. Trekket er en egen ting, og du må opprette det selv, hver periode.

Avtale, trekk og betalingTre objekter: avtalen godkjennes én gang hos Vipps, trekket opprettes av din egen cron for hver periode, og betalingen er din egen bokføring.Agreementrecurring/v3, hos VippsÉn per abonnent.Kunden godkjennerén gang.AgreementChargerecurring/v3, hos VippsÉn per periode.Du oppretter den.Vipps gjør det ikke.Paymentdin databaseDin bok.Speiler det Vippssier er sant.cronsync↑ dette steget finnes ikke hos Vipps — din cron lager det 3 dager før forfall
Vipps lager ikke trekkene for deg. Kommer du fra Stripe Subscriptions er det den største forskjellen: avtalen er bare et samtykke. Noen må be om pengene hver periode, og det er din cron.

Det betyr at et abonnement i Vipps krever en cron du eier. Repoet lager trekket tre dager før forfall, fordi Vipps krever at forfallsdatoen ligger et par dager fram i tid. Går kjøringen én dag, lager den ett trekk per avtale som har forfall innen vinduet — ikke ett per periode som har passert.

Mønster 2: nøkkelen må komme fra noe du eier

Dette er tyngdepunktet. Vipps krever en Idempotency-Key på trekk, og et nytt forsøk med samme nøkkel gir deg samme trekk i stedet for et nytt. Problemet oppstår ikke når nøkkelen mangler — det oppstår når et bibliotek genererer en fersk UUID per kall. Da er et nytt forsøk ikke et nytt forsøk for Vipps. Det er en ny betaling.

Idempotens ved nytt forsøkTo baner: en nøkkel utledet av din egen varige id gir samme trekk ved nytt forsøk, mens en tilfeldig nøkkel per forsøk gir to trekk.Nøkkel fra egen idsub-clx7…-2026-09-01Samme nøkkel begge ganger → Vipps svarer med SAMME trekk1 belastningNøkkel fra randomUUID()a3f1… / 9c02…Ny nøkkel per forsøk → Vipps ser to ulike operasjoner2 belastningerCRON KJØRER TO GANGER — KRASJ FØR SVARET REKKER FRAMreference = `sub-${agreement.id}-${ymd(agreement.nextChargeDate)}` — planlagt periode, ikke klokkeslett
Faren er ikke at kallet feiler — det er vinduet mellom at Vipps har opprettet trekket og at du har lagret det. Dør prosessen der, må neste forsøk bære nøyaktig samme nøkkel. Derfor må den utledes av noe du eier fra før, aldri genereres i øyeblikket.

Legg merke til hva nøkkelen er utledet av: vår egen avtale-id og den planlagte perioden. Ikke Vipps' avtale-id, som er sirkulær — nøkkelen må finnes før første vellykkede kall. Og ikke forfallsdatoen, som skyves fram når cronen er forsinket. Bruker du den, endrer nøkkelen seg i akkurat den situasjonen idempotensen skulle redde deg fra.

src/server/agreements.ts
// Nøkkelen utledes av noe vi eier fra før — vår egen
// avtale-id og den PLANLAGTE perioden. Aldri randomUUID(),
// og aldri forfallsdatoen, som flytter seg når cronen er sen.
const reference =
  `sub-${agreement.id}-${ymd(agreement.nextChargeDate)}`;

// Finnes den allerede lokalt, har vi gjort dette før.
const existing = await db.agreementCharge.findUnique({
  where: { reference },
});
if (existing) continue;

await createCharge({
  msn,
  agreementId: agreement.vippsId,
  reference,
  amountOre: agreement.amountOre,
  due: dueDate,
  description: agreement.description,
});

Det er to lag her, og de gjør ikke samme jobb. Oppslaget på reference i din egen base er den billige veien ut: kjenner du trekket fra før, slipper du å ringe Vipps i det hele tatt, og den unike indeksen hindrer to rader for samme trekk. Idempotensnøkkelen er det som redder deg når du ikke vet — enten fordi prosessen døde mellom Vipps-kallet og din commit, eller fordi to kjøringer er i luften samtidig og ingen av dem har rukket å lagre noe. Da er nøkkelen det eneste Vipps har å kjenne forsøket igjen på.

Samme resonnement gjelder capture og refusjon. I repoet er handlingen en del av nøkkelen — capture-<id> mot refund-<id> — og funksjonen tar nøkkelen som et påkrevd argument uten standardverdi. Det er et bevisst valg: en feil du ikke kan kompilere er bedre enn en kommentar som ber deg passe på.

Mønster 3: status er noe du henter

Webhooken forteller deg at noe har skjedd. Den er ikke beviset på hva. Signaturen kan du verifisere med HMAC, og den bør du sette opp — men selv en verifisert webhook leveres minst én gang, uten rekkefølgegaranti, og av og til ikke i det hele tatt. En captured-hendelse kan lande før authorized.

Betalingsflyt: hvor status kommer fraAppen oppretter en betaling hos Vipps, kunden godkjenner i Vipps-appen, Vipps sender en webhook som kun er et signal, og appen henter deretter autoritativ status fra Vipps.Kundenettleser + Vipps-appDin appNext.js · tRPCVippsePayment API1. Opprett betaling2. Til Vipps-appen3. Webhook: «noe skjedde»signert, men ikke bevis4. Hent status — dette er fasiten
Den grå pilen er ikke bevis. Webhooken kan signeres, men den leveres minst én gang, uten rekkefølgegaranti, og av og til ikke i det hele tatt. Redirecten styres av brukeren. Begge er signaler om å gå og spørre Vipps. Den grønne pilen er det eneste stedet appen henter sannheten om at penger faktisk har flyttet seg.

Derfor er den daglige kjøringen ikke bare en trekkmotor, den er også backup for tapte hendelser. Det er også grunnen til at repoet tør kjøre signaturvalidering i varselmodus som standard — avvik logges, flyten fortsetter — og at et oppsett uten lagret hemmelighet ikke stopper noe. Slå på håndheving når produksjonsloggene bekrefter at signaturene verifiserer. Poenget er at tilliten uansett ikke hviler der: den hviler på det autentiserte oppslaget.

Én detalj som er lett å overse: sideeffekter må være like idempotente som pengene. Når en betaling går fra ubetalt til betalt, oppdateres raden med en betingelse om at den ikke allerede er betalt. Da sendes kvitteringen nøyaktig én gang, uansett hvor mange webhooks og pollinger som lander samtidig.

Mønster 4: AUTHORIZED betyr ikke «ikke trukket»

Den detaljen som overrasker de fleste: en Vipps-betaling blir stående i AUTHORIZED også etter at du har trukket den. Tilstanden flytter seg ikke ved capture.

state mot aggregateTilstanden AUTHORIZED står stille gjennom hele forløpet, mens beløpsfeltene i aggregate endrer seg fra null til trukket beløp.state: AUTHORIZEDuendret fra kunden godkjenner til betalingen er ferdigaggregate.capturedAmount:025000capture endret tallet — ikke tilstandenDIN AVLEDNINGcapturedOre > 0 → PAIDcapturedOre === 0 → RESERVERTVipps sender også cancelledAmount og refundedAmount på samme objekt. Alt du trenger ligger der.
Tilstanden flytter seg ikke. Tallene gjør. Du fører ikke egne tall ved siden av — du speiler aggregate fra Vipps. Leser du bare state, ser et trukket beløp ut som et utrukket.

Løsningen er ikke å telle selv. Vipps sender beløpene på samme objekt — autorisert, trukket, kansellert og refundert — så du speiler dem. Egen parallell telling er nettopp det som driver ut av synk.

Hva som ligger i repoet

Alt over er hentet fra en integrasjon som kjører: ePayment med reservasjon, trekk og refusjon, Recurring med fornyelsescron, QR, Vipps Login, signerte webhooks og avstemming mot Report API.

Den delen som er vanskeligst å få riktig, og som jeg vil trekke fram særskilt, er partnermodellen: hver organisasjon har sitt eget salgssted hos Vipps, med egne webhooks, og pengene går rett til dem — aldri via plattformen. Det er den arkitekturen som gjør at du kan bygge et flerkundeprodukt uten å komme i nærheten av konsesjonsplikt, og det er den som er mest arbeid å komme fram til selv.

Hva den ikke gjør

  • — Enhetstestene dekker idempotensnøkler, auth-policy og rate limiting. Trekkmotoren og statussynkroniseringen er dekket av e2e-tester, ikke av enhetstester.
  • — Ingen purrelogikk. Vipps prøver et feilet trekk i fem dager; hva som skjer med abonnementet etter det, må du bestemme selv.
  • — Express er bygget, men avslått som standard til et ekte produkt og levering er satt opp.
  • — Agentic Commerce er en kontrakt, ikke en integrasjon. Ingen produksjonskall.
  • — Den forutsetter Prisma og PostgreSQL. Vipps-modulene i src/server/ er derimot uavhengige av resten av appen.

Kom i gang

Fra klon til første testbetaling i Vipps-appen er det tre kommandoer og en .env. Alt peker mot testmiljøet som standard, så du kan ikke flytte ekte penger ved et uhell.

Kom i gang
git clone https://github.com/aridder/vipps-starter
cd vipps-starter
./scripts/dev setup   # .env, Postgres, migrasjoner og seed
npm run dev

Vil du se det virke før du kloner: demoen tar imot ekte Vipps-betalinger. Det er nøyaktig koden over, inkludert webhooken.

Ta det du trenger

Hele integrasjonen er MIT-lisensiert. Klon den, løft én modul, selg det du bygger med den — uten å spørre.

Jeg tar oppdrag på Vipps-integrasjon, betalingsflyt og Next.js. Bruker du koden i noe, hører jeg gjerne om det — issues og e-post er åpne.

Alle påstander er sjekket mot primærkilder 28. august 2026, og lenkene går til Vipps', Stripes og Mollies egen dokumentasjon. Har noe endret seg siden, si fra — en tekst om betaling som lyver er verre enn ingen tekst.