Spring til indhold
Forta Pay

Reference

Hændelser og webhooks

Forta Pay API v1

13 operationer

Ændringsfeed — alle hændelser for kontoen i rækkefølge. Fortsæt fra sidste cursor

GET /events

Parametre

cursor

query · string · valgfri

limit

query · integer · valgfri

type

query · string · valgfri

Filtrér på hændelsestype (prefix tilladt, fx invoice.)

creditor_id

query · string · valgfri

Svar

200

OK · Page

curl
curl -X GET https://api.fortapay.dk/v1/events \
  -H "Authorization: Bearer $FP_KEY"

Afstemning — alt der er ændret siden markør X, med et skrevet løfte om at der ikke er huller

GET /changes

Samme rækker som /events, men med den garanti en afstemning kan hvile på:

«Kalder du med markør X, får du hver hændelse med seq > X, i rækkefølge, uden dubletter og uden huller, til og med complete_through = Y.»

Gemmer du Y og kalder igen, begynder næste svar over Y. Ingen hændelse med seq ≤ Y kan blive synlig efter du har fået Y — rækkefølgen er commit-rækkefølgen, fordi nummeret tildeles under kontoens rækkelås. En tabt webhook bliver dermed en FORSINKELSE, ikke et TAB.

Forskellen fra /events — og hvorfor den findes. /events' cursor er et hændelses-id og udelades (null) når du er ajour; er du ajour eller har feedet tabt hændelsen, har du intet at gemme. /changes returnerer ALTID en markør, også på et tomt svar, og markøren er et tal der overlever at hændelsen selv forsvinder.

Hvad sker der når kaldet udebliver? Ingenting går tabt: markøren står stille, og næste kald leverer alt det mellemliggende. Der er ingen tidsfrist på et ubrugt kald.

Hvad hvis du mister markøren? Så er du på egen hånd — og det er et gyldigt svar, men det skal stå skrevet: der findes ingen «hvor var jeg»-genopretning. Du har to veje, og de koster hver sit: (a) kald uden cursor og genafspil feedet fra begyndelsen — korrekt, men hele historikken; (b) hent en frisk markør på GET /changes/cursor og genindlæs de ressourcer du har brug for via listekaldene — billigere, men du får ALDRIG de hændelser der ligger imellem.

Hvad hvis markøren er for gammel? 410 cursor_too_old, aldrig et afkortet svar. Serveren kender det højeste fjernede seq (accounts.events_pruned_through) og nægter at svare på en markør derunder, fordi svaret ville have et hul. I dag fjernes hændelser kun af POST /sandbox/reset; indføres en opbevaringsgrænse, er det den samme 410.

Hvad hvis markøren hører til en anden konto? 409 cursor_wrong_account. Markøren bærer kontoen, netop fordi et blot tal ville blive besvaret pænt og fuldstændigt på den forkerte konto — sandbox-markøren der følger med konfigurationen over i live er den typiske.

Hvad hvis markøren hører til en nøgle med et andet kreditor-skop? 409 cursor_wrong_scope (siden 17-09-2026). Markøren bærer et AFTRYK af nøglens kreditor-skop — ikke skopet selv, så markøren røber ikke hvad nøglen kan se. Uden det ville en markør hentet med en nøgle skopet til kreditor A og brugt med en nøgle skopet til B blive besvaret 200: B ville få sine hændelser OVER markøren, og alt B havde UNDER den ville være tavst væk for altid. Aftrykket er bundet til MÆNGDEN af kreditorer, ikke til rækkefølgen: to nøgler med samme kreditorer i forskellig orden deler markør.

Hvad hvis markøren hører til et andet filter? 409 cursor_wrong_filter (siden 18-09-2026). Markøren bærer også et aftryk af det filter (type/creditor_id) kaldet blev gjort med. Uden det ville en markør hentet med ?type=invoice.* og brugt uden filter (eller omvendt) blive besvaret 200 — og de hændelser der lå UNDER markøren og først matcher det nye filter, ville være tavst væk for altid. Egen kode, fordi «skiftet filter» og «forkert nøgle» er to forskellige rettelser i din log. De to stavemåder invoice.* og invoice. er SAMME filter og deler markør. Næste skridt: hent en frisk markør på GET /changes/cursor med SAMME filter som du kalder /changes med — eller før én markør pr. filter.

Hvad hvis markøren er fra før 17-09-2026? 409 cursor_missing_scope (før skop-aftrykket; to-delte markører mangler begge aftryk og får den ældste mangels kode). Er den fra 17.–18.-09-2026 (med skop-aftryk, uden filter-aftryk): 409 cursor_missing_filter. Markører fra ældre formater kan ikke afgøres gyldige og accepteres ikke — der er ingen overgangsordning, fordi et format uden aftryk der forbliver lovligt, er den dør fejlene ovenfor kommer ind ad igen. Næste skridt er det samme som ved de andre 409'ere: hent en frisk markør på GET /changes/cursor MED DEN NØGLE og MED DET FILTER du vil kalde med, og genindlæs det du mangler under den via listekaldene.

Hvad hvis markøren er FORUD for feedet? 409 cursor_ahead_of_feed — markøren hører til en anden konto eller et miljø der er gendannet fra backup. Et tomt svar ville ligne «ajour».

Huller i NUMRENE er ikke huller i feedet. Ruller en transaktion tilbage efter at have taget sit nummer, findes nummeret aldrig. Løftet er «ingen hændelse springes over», ikke «numrene er sammenhængende» — tæl ikke på sammenhæng.

Markøren hører til nøglens skop og til kaldets filter — og BEGGE håndhæves. En kreditor-begrænset nøgle ser kun sine egne hændelser; skopet afvises med 409 cursor_wrong_scope, filteret (siden 18-09-2026) med 409 cursor_wrong_filter. Kalder du med type=/creditor_id=, dækker markøren kun DET filter; vil du følge flere filtre, så før én markør pr. filter — de afviser hinanden i stedet for tavst at springe hændelser over.

⚠️ complete_through er feedets position, ikke dit højeste seq. Er du filtreret — af nøglens kreditor-skop eller af type=/creditor_id= — rykker markøren frem forbi de hændelser, filteret sorterede fra, når siden er fuldstændig. Tallet er derfor normalt HØJERE end den sidste hændelse i data, og det er præcis det, der gør et tomt svar til et fremskridt i stedet for en evig genscanning. Se GET /changes/cursor for konsekvensen.

Parametre

cursor

query · string · valgfri

limit

query · integer · valgfri

type

query · string · valgfri

Filtrér på hændelsestype (prefix tilladt, fx invoice.). Markøren bærer filteret og gælder KUN dette filter — et andet filter afvises med 409 cursor_wrong_filter

creditor_id

query · string · valgfri

Filtrér på kreditor. Indgår i markørens filter-aftryk, ligesom type

Svar

200

OK · ChangeFeedPage

400

Fejl (RFC 9457) · Problem

409

`cursor_wrong_account` (markøren er udstedt til en anden konto — fx sandbox-markøren brugt mod live), `cursor_wrong_scope` (markøren er udstedt til en nøgle med et andet kreditor-skop), `cursor_wrong_filter` (markøren er udstedt til et andet `type`/`creditor_id`-filter end kaldets), `cursor_missing_scope` (markøren er fra før 17-09-2026 og bærer intet skop-aftryk), `cursor_missing_filter` (markøren er fra 17.–18.-09-2026 og bærer intet filter-aftryk) eller `cursor_ahead_of_feed` (markøren er højere end kontoens hændelsestæller). Alle seks: hent en frisk markør på `GET /changes/cursor` med DENNE nøgle og SAMME filter · Problem

410

`cursor_too_old` — hændelserne efter markøren findes ikke mere; svaret ville have et hul · Problem

curl
curl -X GET https://api.fortapay.dk/v1/changes \
  -H "Authorization: Bearer $FP_KEY"

Markøren «lige nu» — til at begynde fra i stedet for fra begyndelsen

GET /changes/cursor

Til to formål: en ny integration der ikke vil genafspille historikken, og en genanskaffelse efter et tab af markøren. 🔴 Begge tilfælde SPRINGER alt før markøren over — det er en beslutning om at give afkald på historikken, ikke en genopretning. Skal du bruge det der ligger før, er vejen GET /changes uden cursor.

🔴 Tallet er kontoens hændelsestæller — ikke «den højeste seq DU kan se». Er nøglen begrænset til én kreditor, tæller kontoens tæller også de andre kreditorers hændelser, og seq er derfor normalt HØJERE end noget, din nøgle nogensinde kan hente. Det samme gælder complete_through i /changes. Det er med vilje: markøren er feedets position, ikke en kvittering for en række du har fået. Den kan ikke springe noget over, som du VILLE have fået — skopet filtrerer rækkerne, ikke rækkefølgen, og de numre du ikke må se, kan du heller aldrig få udleveret senere.

⚠️ Sammenlign derfor ikke seq med den sidste hændelse du har set. Et spring fra 40 til 112 betyder ikke, at du mangler 72 hændelser. Det betyder, at 72 numre gik til noget, din nøgle ikke har adgang til — eller til transaktioner, der rullede tilbage (se «huller i NUMRENE» under /changes). Den eneste rigtige «er jeg ajour?»-prøve er has_more: false.

⚠️ Hvad tallet da røber, sagt lige ud: hvor mange hændelser der har været på KONTOEN, også de kreditorers du ikke må se. Det er et aktivitetstal inden for én konto, ikke data om nogen. Skal to kreditorer ikke kunne se hinandens aktivitetsniveau, skal de have hver sin KONTO — ikke hver sin nøgle på samme konto.

🔴 Markøren bærer DET kreditor-skop, kaldet blev gjort med (siden 17-09-2026). Hent den altså med den nøgle, du vil kalde /changes med — en markør hentet med én nøgle og brugt med en nøgle med et andet skop afvises med 409 cursor_wrong_scope. To nøgler med samme kreditorer (i hvilken som helst rækkefølge) kan bruge hinandens markør.

🔴 Og markøren bærer DET filter, kaldet blev gjort med (siden 18-09-2026). Endepunktet tager derfor de samme type/creditor_id-argumenter som /changes: hent markøren med det filter, du vil kalde /changes med — ellers afvises den med 409 cursor_wrong_filter. Uden argumenterne her ville den afvisning være en blindgyde: du kunne aldrig FÅ en markør, der passer til dit filter. De to stavemåder invoice.* og invoice. er samme filter.

Parametre

type

query · string · valgfri

Samme filter som på GET /changes — markøren bærer det

creditor_id

query · string · valgfri

Samme filter som på GET /changes — markøren bærer det

Svar

200

OK · ChangeCursor

curl
curl -X GET https://api.fortapay.dk/v1/changes/cursor \
  -H "Authorization: Bearer $FP_KEY"

Hent én hændelse (samme payload som webhooken)

GET /events/{eventId}

Parametre

eventId

path · string · påkrævet

Svar

200

OK · Event

curl
curl -X GET https://api.fortapay.dk/v1/events/{eventId} \
  -H "Authorization: Bearer $FP_KEY"

Liste (uden secret)

GET /webhook-endpoints

Parametre

cursor

query · string · valgfri

limit

query · integer · valgfri

Svar

200

OK · Page

curl
curl -X GET https://api.fortapay.dk/v1/webhook-endpoints \
  -H "Authorization: Bearer $FP_KEY"

Opret endpoint. Hemmeligheden returneres ÉN gang og står aldrig i URL'en. Signering er altid slået til

POST /webhook-endpoints

Parametre

Idempotency-Key

header · string (uuid) · påkrævet

Obligatorisk på alle POST/PATCH/DELETE. Lever 24 timer.

Krop · WebhookEndpointCreate

mode

string · valgfri

http (standard): levering over https til url. inspector: ingen URL, sendes aldrig over nettet — leveringen registreres som leveret og ses med GET /webhook-deliveries/{id}. KUN sandbox (422 inspector_sandbox_only i live). Kan ikke ændres bagefter

http · inspector

creditor_id

string · valgfri

Kreditor-skop. Sat = endpointet hører til ÉN kreditor og modtager KUN hændelser med netop den creditor_id — heller ikke konto-niveau-hændelser (creditor_id er tom). Udeladt = kontoens endpoint, som ser alt kontoen ser. Kan ikke ændres bagefter

url

string (uri) · valgfri

https kræves ved mode=http (påkrævet dér); udelades ved mode=inspector. Hemmeligheden er IKKE en del af URL'en

event_types

liste af string · påkrævet

Fx ["invoice.*", "reminder.sent", "collection_case.*"]

description

string · valgfri

api_version

string · valgfri

Svar

201

Oprettet (med `secret`) · WebhookEndpoint

curl
curl -X POST https://api.fortapay.dk/v1/webhook-endpoints \
  -H "Authorization: Bearer $FP_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d @- <<'JSON'
{
  "event_types": [
    "…"
  ]
}
JSON

Hent (uden secret)

GET /webhook-endpoints/{endpointId}

Parametre

endpointId

path · string · påkrævet

Svar

200

OK

curl
curl -X GET https://api.fortapay.dk/v1/webhook-endpoints/{endpointId} \
  -H "Authorization: Bearer $FP_KEY"

Ret URL/typer/beskrivelse, eller slå endpointet fra/til (`status`)

PATCH /webhook-endpoints/{endpointId}

Parametre

endpointId

path · string · påkrævet

Idempotency-Key

header · string (uuid) · påkrævet

Obligatorisk på alle POST/PATCH/DELETE. Lever 24 timer.

Krop · WebhookEndpointUpdate

url

string (uri) · valgfri

event_types

liste af string · valgfri

description

string · valgfri

status

string · valgfri

disabled_by_user slår leveringen fra; active slår den til igen (også efter disabled_by_failures)

active · disabled_by_user

Svar

200

OK

curl
curl -X PATCH https://api.fortapay.dk/v1/webhook-endpoints/{endpointId} \
  -H "Authorization: Bearer $FP_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d @- <<'JSON'
{}
JSON

Slet

DELETE /webhook-endpoints/{endpointId}

Parametre

endpointId

path · string · påkrævet

Idempotency-Key

header · string (uuid) · påkrævet

Obligatorisk på alle POST/PATCH/DELETE. Lever 24 timer.

Svar

204

Slettet

curl
curl -X DELETE https://api.fortapay.dk/v1/webhook-endpoints/{endpointId} \
  -H "Authorization: Bearer $FP_KEY" \
  -H "Idempotency-Key: $(uuidgen)"

Rotér hemmelighed — gammel og ny er begge gyldige i 24 timer (to signaturer i headeren)

POST /webhook-endpoints/{endpointId}/rotate-secret

Parametre

endpointId

path · string · påkrævet

Idempotency-Key

header · string (uuid) · påkrævet

Obligatorisk på alle POST/PATCH/DELETE. Lever 24 timer.

Svar

200

Ny secret returneres én gang

curl
curl -X POST https://api.fortapay.dk/v1/webhook-endpoints/{endpointId}/rotate-secret \
  -H "Authorization: Bearer $FP_KEY" \
  -H "Idempotency-Key: $(uuidgen)"

Leveringshistorik (forsøg, svarkode, næste forsøg)

GET /webhook-endpoints/{endpointId}/deliveries

Parametre

endpointId

path · string · påkrævet

cursor

query · string · valgfri

limit

query · integer · valgfri

Svar

200

OK

curl
curl -X GET https://api.fortapay.dk/v1/webhook-endpoints/{endpointId}/deliveries \
  -H "Authorization: Bearer $FP_KEY"

Webhook-inspektøren (WH-INSP): forsøgene + den GENSKABTE request (headers, krop, signeret streng `id.ts.body`). Kroppen gemmes ikke; den genskabes fra hændelsen, og `body_matches` beviser byte-ensheden mod den sha256 der blev logget ved afsendelsen. `false` = hændelsen er ændret siden (så er `request` null). En kreditorbegrænset nøgle (write) når sine kreditorers endpoints; kontoens eget endpoint → 403

GET /webhook-deliveries/{deliveryId}

Parametre

deliveryId

path · string · påkrævet

Svar

200

OK · WebhookDeliveryInspection

curl
curl -X GET https://api.fortapay.dk/v1/webhook-deliveries/{deliveryId} \
  -H "Authorization: Bearer $FP_KEY"

Gensend én levering (samme event-id, ny timestamp/signatur) som en NY generation med eget retry-budget. Højst én aktiv: en levering der allerede er `pending` → 409 `delivery_in_progress`; et endpoint der ikke er aktivt → 409 `endpoint_disabled`

POST /webhook-deliveries/{deliveryId}/redeliver

Parametre

deliveryId

path · string · påkrævet

Idempotency-Key

header · string (uuid) · påkrævet

Obligatorisk på alle POST/PATCH/DELETE. Lever 24 timer.

Svar

202

Planlagt

curl
curl -X POST https://api.fortapay.dk/v1/webhook-deliveries/{deliveryId}/redeliver \
  -H "Authorization: Bearer $FP_KEY" \
  -H "Idempotency-Key: $(uuidgen)"