API

Guide 8 af 8

Implementering

Kogebog til at flytte en advokatvirksomhed ind i Levano. Skrevet til den der sidder med migreringen og skal kunne stå på mål for tallene bagefter, ikke til den der skal overbevises om noget.

Den er delt i to kapitler:

Kapitel Kan du i dag
1. Recon og afstemning Ja. Alt herunder kræver kun læse-scopes
2. Import Ja. POST med Idempotency-Key

Rækkefølgen er ikke et kompromis. Afstemningen i kapitel 1 er den samme kode du bruger til at kontrollere importen bagefter, så den er ikke spildt arbejde fordi den kommer først.

Kapitel 1: Recon og afstemning

Målet er tre ting, i den rækkefølge:

  1. Baseline. Præcis hvad ligger der i organisationen lige nu.
  2. Nøglekort. Hvilken række i kildesystemet svarer til hvilken række i Levano.
  3. Afstemning. Stemmer antal og beløb, og hænger referencerne sammen.

Forudsætninger

En API-nøgle med de rigtige scopes, oprettet af en administrator under Indstillinger, API-nøgler: https://app.levano.io/settings/api.

Vælg scopes én gang og vælg dem rigtigt. Et scope kan ikke sættes på en nøgle bagefter. Mangler du ét midt i et udtræk, skal du oprette en ny nøgle og starte forfra, så mint hele læsesættet med det samme:

Scope Giver dig
clients:read /clients, /people, /companies
matters:read /matters
time:read /time-entries
invoices:read /invoices
documents:read /documents, kun metadata
reports:read /reports/{type}, den uafhængige kontrol

webhooks:manage hører ikke til en migrering. Det er den eneste familie i v1 der kan skrive, og den skriver ikke sagsdata.

export LEVANO_KEY="lev_live_..."
export LEVANO_API="https://app.levano.io/api/v1"

Nøglen er et kodeord til hele organisationens sagsdata. Den skal ikke i et repository, ikke i en ticket og ikke i en log.

Preflight: kontrollér nøglen før du henter noget

Et kald pr. liste, med per_page=1, koster syv kald af rate limit-budgettet og sparer dig for at opdage et manglende scope efter tyve minutters udtræk.

for endpoint in clients people companies matters time-entries invoices documents; do
  code=$(curl -sS -o /dev/null -w '%{http_code}' "$LEVANO_API/$endpoint?per_page=1" \
    -H "Authorization: Bearer $LEVANO_KEY" \
    -H "Accept: application/json")
  printf '%s  /%s\n' "$code" "$endpoint"
done

200 betyder at scopet er der. 403 betyder at det mangler, og svarets required_scope siger hvilket. 401 betyder at nøglen selv er forkert, spærret eller udløbet, og at intet af resten virker.

Rækkefølgen

Hent i den rækkefølge tingene peger på hinanden, så en reference altid er hentet før den bliver brugt.

# Endpoint Peger på
1 /people, /companies Ingenting. Parterne er bunden
2 /clients company_id eller person_id fra 1
3 /matters client_id fra 2
4 /time-entries matter_id fra 3
5 /invoices client_id fra 2, matter_id fra 3
6 /documents matter_id fra 3

Det er også importrækkefølgen i kapitel 2. Den ændrer sig ikke fordi retningen gør.

Klient er et engagement, ikke en part

Det her er den ene modelforskel der vælter en migrering hvis den opdages sent.

En klient i Levano er et engagement: relationen mellem virksomheden og en part. Stamdata, altså navn, adresse, e-mail, CVR, ligger ikke på klienten. Den ligger på parten, som er en række i /people eller /companies, og klienten peger på den med person_id eller company_id.

/clients viser stamdata med, men det er en afledt visning af parten. Den autoritative række er partens.

Konsekvensen er konkret: den samme virksomhed kan holde flere engagementer, og så er der én partrække og flere klientrækker. Nøgler du dit kort på klienten, opretter du den samme virksomhed to gange i kildesystemet og får to sæt stamdata der langsomt driver fra hinanden.

Nøgl på parten. Klienten er relationen.

Naturlige nøgler til kortet

Der er ingen "ekstern id"-kolonne i v1, så kortet mellem kildesystem og Levano bygges på de felter der faktisk identificerer en række.

Enhed Match på Note
Virksomhed cvr /companies?cvr=12345678 matcher på ciffer, så 12 34 56 78 finder den samme række
Person name + email Der er intet CPR på ledningen. Se nedenfor
Klient Parten plus type Bruger virksomheden e-conomic, er economic_customer_number ofte den bedste nøgle
Sag matter_number, ellers reference reference er et frit tekstfelt til klientens egen reference, og den stærkeste nøgle når det blev udfyldt ved indlæsningen
Tid matter_id + date + description Der er ingen naturlig nøgle. Se nedenfor
Faktura invoice_number Entydig inden for organisationen, men tom på kladder. En kladde matches ikke på nummer
Dokument matter_id + filename size_bytes og mime skiller de fleste dubletter

Personer kan ikke matches maskinelt på CPR. CPR er ikke i svaret, i nogen form, og kommer det ikke. Er der to Jens Hansen uden e-mail i registret, er det et menneske der afgør hvilken der er hvilken. Regn med det i planen frem for at opdage det i uge tre.

Tidsregistreringer har ingen naturlig nøgle. Kombinationen ovenfor er heuristik, ikke identitet: to registreringer på samme sag samme dag med samme tekst er lovlige. Til afstemning tæller og summerer du derfor pr. sag frem for at parre række mod række.

Udtrækket

Listerne er cursor-paginerede. Reglerne, kort, fordi de bestemmer om udtrækket er komplet:

  • Der er ingen page-parameter og ingen total. Du følger links.next indtil den er null.
  • links.next er en absolut URL med dine filtre bevaret. Du sætter den ikke sammen selv.
  • per_page er 50 som standard og maksimum 100. Beder du om mere, får du 100. Beder du om nul eller negativt, får du 50. Ingen af delene er en fejl, så du får ikke besked.
  • Cursoren er uigennemsigtig. Gem den ikke som et bogmærke du vender tilbage til i næste uge.
  • Sorteringen er nyeste først med id som tiebreaker, og der er ingen sort-parameter.

Detaljerne står i Paginering.

dump () {
  # `endpoint`, not `path`: zsh ties a `path` variable to `PATH`, so the
  # innocent-looking name empties your PATH for the rest of the function.
  local endpoint="$1" out="$2" url response code body
  url="$LEVANO_API/$endpoint?per_page=100"
  : > "$out"

  while [ -n "$url" ] && [ "$url" != "null" ]; do
    response=$(curl -sS -w '\n%{http_code}' "$url" \
      -H "Authorization: Bearer $LEVANO_KEY" \
      -H "Accept: application/json")
    code=${response##*$'\n'}
    body=${response%$'\n'*}

    if [ "$code" != "200" ]; then
      printf '%s: afbrudt på HTTP %s: %s\n' "$endpoint" "$code" "$body" >&2
      return 1
    fi

    printf '%s' "$body" | jq -c '.data[]' >> "$out"
    url=$(printf '%s' "$body" | jq -r '.links.next')
  done

  printf '%8s  %s\n' "$(wc -l < "$out" | tr -d ' ')" "$out"
}

dump people       personer.ndjson
dump companies    virksomheder.ndjson
dump clients      klienter.ndjson
dump matters      sager.ndjson
dump time-entries tid.ndjson
dump invoices     fakturaer.ndjson
dump documents    dokumenter.ndjson

Statuskontrollen i løkken er ikke pynt. Uden den er den her løkke farlig: et 429 midtvejs giver et svar uden data og uden links.next, jq skriver ingenting, url bliver null, og løkken stopper stille med en halv fil. Det ser i afstemningen ud som om der mangler data i Levano, og du leder det forkerte sted i en halv dag. Stop på alt der ikke er 200, og genstart det ene udtræk.

Vil du hellere genforsøge end stoppe: 429 bærer en Retry-After i sekunder, og den skal læses frem for gættes. Se Fejl og rate limits.

Rate limit tælles pr. nøgle, ikke pr. IP, som standard 120 kald i minuttet. Kører du udtrækket parallelt fra fire maskiner med den samme nøgle, deler de ét budget. per_page=100 henter det samme som fire kald med per_page=25 for ét kald af budgettet, så den største side er også den billigste.

Beløbene i udtrækket

Hvert beløb er et heltal i øre plus en currency. Aldrig en decimal, aldrig et flydende tal, aldrig en formateret streng. 162375 med currency: "DKK" er 1.623,50 kr.

{
  "invoice_number": "2026-0184",
  "status": "sent",
  "subtotal": 1299000,
  "vat_amount": 324750,
  "total": 1623750,
  "paid_amount": 0,
  "outstanding": 1623750,
  "vat_rate": 0.25,
  "currency": "DKK"
}

Hvilke felter der er beløb, og hvilke der ligner:

Er øre Er ikke øre
Sag: hourly_rate, fixed_fee_amount, budget_amount vat_rate er en brøk. 0.25 er 25 procent
Tid: hourly_rate, internal_rate, amount, internal_amount duration_minutes er minutter
Faktura: subtotal, vat_amount, total, paid_amount, outstanding, procesrente_amount Fakturalinje: quantity er et antal
Fakturalinje: unit_price, amount Dokument: size_bytes er bytes

Tre regler der holder afstemningen ren:

  1. Summér i heltal. Divider aldrig med 100 før du lægger sammen. Et beløb der er blevet til en float på vejen kommer ikke tilbage som et heltal.
  2. Gem heltal. Læser du øre ind i en float for at gemme dem i en DECIMAL(10,2), har du genindført præcis det problem heltallet fjernede.
  3. Genberegn ikke moms. vat_amount er det tal Levano har bogført, inklusive momszone og eventuel omvendt betalingspligt. subtotal * vat_rate kan lovligt afvige, og fakturaen er facit.

Baggrunden står i Beløb.

Tæl

for f in personer virksomheder klienter sager tid fakturaer dokumenter; do
  printf '%8s  %s\n' "$(wc -l < "$f.ndjson" | tr -d ' ')" "$f"
done

Sæt tallene op mod kildesystemet med det samme, før du kigger på beløb. Et antal der er langt forkert er næsten altid et udtræk der stoppede for tidligt, ikke data der mangler i Levano, og det er billigere at opdage nu.

Bemærk at /clients og /companies ikke skal have samme antal, og at ingen af dem er den andens loft. /clients tæller engagementer, /companies og /people tæller parter. Én part kan holde flere engagementer, og en part kan ligge i registret uden at holde nogen. Det der skal holde, er referencerne, og dem tager næste afsnit.

Afstem referencerne

Inden for én organisation er referencerne garanteret af databasen. Peger en sag i dit udtræk på en klient du ikke har, betyder det derfor én af to ting, og aldrig at Levano har mistet noget.

jq -r '.id' klienter.ndjson | sort -u > .klient-id
jq -r '.client_id // empty' sager.ndjson | sort -u > .sag-klient
comm -13 .klient-id .sag-klient

Tom output er rent. Kør det samme par for matter_id fra tid, dokumenter og fakturaer, og for company_id og person_id fra klienter.

// empty i filteret er med vilje: null er en lovlig værdi og betyder "ikke knyttet til noget". En tidsregistrering uden sag hører til ingen klient og skal ikke tælle som en brudt reference. Det er et ikke-tomt id du ikke har, der er symptomet.

Hver linje output er så enten:

  1. Et ufuldstændigt udtræk. Løkken stoppede for tidligt. Det er langt det almindeligste, og det er derfor statuskontrollen ovenfor er der.
  2. En slettet forælder. Alle enheder her er soft-deletede. En slettet klient forsvinder fra /clients, men dens sager bliver liggende på /matters med client_id intakt, fordi en sletning af klienten ikke sletter sagerne. Der er intet deleted_at på ledningen og ingen måde at hente den slettede række, så en peger uden mål er alt du får at se.

Kør udtrækket igen før du konkluderer. Er den samme peger uden mål begge gange, er det den anden slags, og så er den rigtige beslutning et produktspørgsmål til virksomheden frem for en teknisk rettelse: hvad skal der ske med en sag hvis klient er slettet.

Afstem beløbene mod Levanos egne tal

/reports/{type} regner over de samme rækker og er derfor en uafhængig anden mening. Det er også det tal virksomheden ser inde i Levano, så det er dét de vil holde dig op på.

Den ene kontrol der er en præcis facitliste:

# Åbne fakturaer i dit udtræk
jq -s 'map(select(
    (.status == "sent" or .status == "partially_paid" or .status == "overdue")
    and .outstanding > 0
  )) | length' fakturaer.ndjson

# Levanos eget tal
curl -sS "$LEVANO_API/reports/ar-aging?date_from=2025-01-01&date_to=2026-09-01" \
  -H "Authorization: Bearer $LEVANO_KEY" -H "Accept: application/json" \
  | jq '.data.stats.open_count'

De to tal skal være ens. Er de ikke, mangler dit fakturaudtræk rækker.

Fælden i rapport-endpointet, og den rammer midt i en afstemning: udelader du date_from og date_to, får du indeværende måned. Ikke alt. En "samlet omsætning" der stille kun dækker september ser ud som katastrofalt datatab når du holder den op mod kildesystemet. Sæt altid begge datoer eksplicit, også når du vil have hele historikken, og hold ellers vinduet til den periode du faktisk skal afstemme.

ar-aging er en undtagelse værd at kende: den aldrer mod dags dato, eller mod reference_date hvis du sætter den, og datovinduet påvirker ikke hvilke fakturaer der tælles med. Derfor er open_count ovenfor uafhængig af hvilke datoer du sender.

Totalerne, altså total_outstandingar-aging og total_wip_valuewip, regnes over rapportens egne rækker, og rækkelisten er afkortet på store datasæt. Brug dem som størrelsesorden og som samtaleåbner med bogholderiet, ikke som facit til øre. wip er derudover afgrænset af datovinduet og tæller kun tid der ikke allerede står på en fakturalinje, så den er ikke det samme tal som en rå sum over tidsudtrækket.

Rapporttyperne: revenue, time-distribution, matter-status, productivity, ar-aging, wip, write-offs, lead-conversion, kyc-status.

Genkør trinvist

Når baseline er på plads, skal du ikke hente alt igen for at se hvad der er ændret. updated_since findes på /matters, /clients, /people og /companies:

curl -sS "$LEVANO_API/matters?updated_since=2026-09-01T00:00:00Z&per_page=100" \
  -H "Authorization: Bearer $LEVANO_KEY" \
  -H "Accept: application/json"

Gem tidspunktet for hvornår du kørte, ikke cursoren. Overlap et par minutter og lad deduplikering på id tage resten. Bruger du et offset frem for Z, skal + URL-kodes som %2B, ellers læses det som et mellemrum.

/time-entries, /invoices og /documents har ikke updated_since. Der afgrænser du i stedet på det du faktisk skal se på: date_from og date_to på tid, issue_date_from og issue_date_to på fakturaer, matter_id på dokumenter.

Typiske fejl

Symptom Årsag Rettelse
403 med required_scope Nøglen mangler det scope Opret en ny nøgle. Scopes kan ikke tilføjes bagefter
Udtrækket stopper stille med en halv fil 429 eller 5xx midt i løkken Kontrollér statuskoden i hver iteration. Læs Retry-After
429 selvom hver proces kalder pænt Grænsen er pr. nøgle, ikke pr. IP Fire parallelle udtræk deler ét budget. Serialisér, eller hæv per_page
per_page=500 giver 100 rækker Loftet er 100, og det er ikke en fejl Forvent 100
Fakturaen har ingen lines Linjer kommer kun med på /invoices/{id} Hent den enkelte faktura når du skal bruge linjerne
/time-entries/{id} giver 404 Der er ikke noget opslag på enkeltregistrering Brug listen med matter_id
Beløb er 100 gange for små Øre blev læst som kroner Alt er heltal i mindste enhed
Momsen afviger en øre subtotal * vat_rate blev genberegnet Brug vat_amount fra svaret
404 på et id kildesystemet kender Rækken findes ikke i denne organisation 404 er ikke "slettet". Slet ikke lokale rækker på ét 404
Et client_id der ikke er i klientudtrækket Afkortet løkke, eller en soft-deletet klient Kør udtrækket igen. Er den der stadig, er forælderen slettet
Rapporten viser en brøkdel date_from og date_to blev udeladt Uden datoer får du indeværende måned
To virksomheder med samme CVR i kortet Der blev nøglet på klienten i stedet for parten Nøgl på company_id eller person_id
updated_since returnerer for meget + i offsettet blev læst som mellemrum URL-kodning: %2B

Hvad API'et ikke kan endnu

Vær ærlig om det her i projektplanen fra dag ét. Det er ikke mangler der lukker sig selv inden fredag.

  • Ingen PATCH eller DELETE på sagsdata. Create-only import findes (se Import). Webhooks er den eneste familie med PATCH/DELETE.
  • Ingen filer. /documents udleverer metadata: navn, filnavn, MIME-type, størrelse, sag og mappe. Aldrig filens indhold, aldrig en download-URL, aldrig et preview.
  • Ingen krypterede noter. Noter på klient og part, og KYC-noter, er krypteret i hvile og kommer ikke med i noget svar.
  • Ingen totaltælling på lister. meta bærer per_page og ikke andet. Skal du bruge tal frem for rækker, er det rapporterne.
  • Ingen sortering. Rækkefølgen er en del af kontrakten fordi cursoren afhænger af den.
  • Ingen slettede rækker. Alt er soft-deletet internt, men der er hverken deleted_at i svaret eller en parameter der henter dem frem. En slettet række er væk fra API'ets synspunkt.
  • settings:read og settings:write åbner ingenting. Navnene findes i kataloget og kan sættes på en nøgle. Byg ikke logik på dem.

CPR er ikke på ledningen

Det fortjener sin egen linje, fordi det både er en teknisk og en juridisk grænse.

CPR indgår ikke i noget svar fra dette API, i nogen form. Ikke som felt, ikke maskeret, ikke som hash du kan slå op på. /people giver navn, e-mail, telefon og adresse, og det er hele partens offentlige flade.

Og den anden vej: CPR importeres ikke gennem dette API. Det gælder også når kapitel 2 lander. Skal en personoplysning af den type ind i Levano, sker det gennem sagsbehandlingen eller KYC-flowet inde i produktet, hvor der er en identificeret bruger der står bag, en audit-log der fanger det, og en kryptering i hvile der holder. En bulk-import over en integrationsnøgle har ingen af de tre.

Praktisk konsekvens for migreringen: læg CPR-matchning ind i planen som manuelt arbejde, ikke som noget scriptet der løser sig selv. Se afsnittet om naturlige nøgler.

Import

Skrivning er create only. Der er hverken PATCH eller DELETE nogen steder i v1: Levano er systemets facit, og en ekstern nøgle skal ikke kunne rette eller slette et klientforhold, en sag eller en faktureret time. POST findes udelukkende fordi en migrering ellers ikke kan lægge sine data ind.

Rækkefølgen

Fire trin, i denne orden, fordi hvert trin peger på rækken før det og ikke kan tryllebinde den frem:

Trin Kald Scope Du får
1 POST /companies eller POST /people parties:write id på parten
2 POST /clients clients:write id på engagementet
3 POST /matters matters:write id på sagen
4 POST /time-entries time:write id på timen

Der findes ikke et kald der laver flere trin på én gang. Det er med vilje: en samlet payload skjuler hvilken halvdel der fejlede, og efterlader dig uden et id at genforsøge imod.

Idempotency-Key er påkrævet

Hvert POST skal have en Idempotency-Key-header. Den er ikke valgfri, for en import man ikke tør genforsøge er en import der går i stå første gang netværket blinker.

Brug kildesystemets eget række-id. Nøglen gælder i 24 timer og er afgrænset pr. endpoint, så det samme række-id godt kan bruges til både virksomheds-kaldet og klient-kaldet bagefter.

Situation Svar
Ny nøgle 201 med den oprettede ressource
Samme nøgle, samme body 200 med Idempotent-Replayed: true. Der blev ikke oprettet noget nyt
Samme nøgle, anden body 409
Samme nøgle mens det første kald stadig kører 409

Et kald der fejlede brænder ikke nøglen. Får du 422, retter du rækken og sender den igen under den samme nøgle.

Trin 1 og 2: parter og klienter

curl -sS -X POST "https://app.levano.io/api/v1/companies" \
  -H "Authorization: Bearer $LEVANO_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: kilde-firma-4711" \
  -d '{"name": "Nordisk Byg ApS", "cvr": "12345678", "city": "Aarhus"}'
curl -sS -X POST "https://app.levano.io/api/v1/clients" \
  -H "Authorization: Bearer $LEVANO_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: kilde-klient-4711" \
  -d '{"company_id": "<id fra trin 1>", "risk_level": "low"}'

Send præcis ét af company_id og person_id. Begge dele, eller ingen af dem, er 422. En part holder højst ét engagement, så et gentaget forsøg er 422 der nævner det engagement der allerede har den.

kyc_status åbner på pending for hvert importeret engagement. En status virksomheden ikke er nået frem til, er ikke en status en importør må påstå.

Trin 3: sager

curl -sS -X POST "https://app.levano.io/api/v1/matters" \
  -H "Authorization: Bearer $LEVANO_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: kilde-sag-4711" \
  -d '{
        "client_id": "<id fra trin 2>",
        "title": "Overdragelse af Nørrebrogade 12",
        "matter_number": "2019-0442",
        "status": "dormant",
        "practice_area": "real_estate",
        "hourly_rate": 250000,
        "opened_at": "2019-04-02"
      }'

client_id og title er de eneste påkrævede felter. To ting er værd at gøre bevidst:

Send matter_number. Udelader du det, tildeler Levano det næste nummer i sin egen SAG-0001-serie. Virksomhedens breve, deres bogholderi og deres klienter refererer til det gamle sagsnummer, og der er ingen PATCH til at sætte det bagefter.

Vælg status med omtanke. Feltet defaulter til active, og hvidvasklovens § 14 gælder for en nøgle nøjagtig som den gælder for en advokat: en sag kan ikke åbne som aktiv mens klientens KYC-sag mangler, ikke er godkendt eller er udløbet. Det er 422status.

Vejen udenom er ikke en undtagelse, men en rækkefølge: importér historiske sager som dormant, closed eller archived, og aktivér dem i Levano når kundekendskabsproceduren er godkendt. Undtagelsen i § 14, stk. 2 kan ikke nås gennem API'et, fordi den er en dokumenteret beslutning truffet af en navngiven person med en bestemt tilladelse, og en nøgle er ingen.

Er sagen lukket, sender du closed_at sammen med status: "closed". Create-kaldet stempler den ikke selv, så uden feltet mister sagen sin lukkedato. closed_at på en sag der ikke er lukket er 422.

legal_domain findes ikke i skemaet. Sporbestemmelsen efter § 1, stk. 1, nr. 13 træffes pr. sag i Levano, og en importeret sag der ikke har fået den beslutning truffet er ærligt udetermineret.

Sætter du budget_amount, tænder du samtidig budgetadvarslerne på sagen. Når de importerede timer i trin 4 passerer 75 procent af budgettet, får den ansvarlige advokat en besked, præcis som hvis timerne var registreret i dag. Det er højst én besked pr. sag, men på en natlig import af flere tusinde sager er det stadig værd at vide på forhånd. Udelad feltet, hvis budgetterne først skal bruges fremadrettet.

Trin 4: tid

curl -sS -X POST "https://app.levano.io/api/v1/time-entries" \
  -H "Authorization: Bearer $LEVANO_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: kilde-tid-88213" \
  -d '{
        "matter_id": "<id fra trin 3>",
        "user_id": "<bruger i organisationen>",
        "date": "2019-04-14",
        "duration_minutes": 90,
        "description": "Gennemgang af skøde",
        "activity_code": "RES",
        "hourly_rate": 250000
      }'

matter_id, user_id, date og duration_minutes er påkrævede.

  • duration_minutes er minutter. Ikke timer, og ikke øre. Nul er 422.
  • hourly_rate kan udelades, og så arver posten sagens egen sats.
  • amount er beregnet ud fra varighed og sats. Sender du den, er det 422 frem for en stille omregning til et andet tal.
  • status åbner altid på draft. invoiced betyder at en fakturalinje i Levano peger på posten, og det gør ingen endnu.
  • activity_code skal være koden på en aktiv aktivitetskode i organisationen. Opret koderne i Levano først.

Der er ikke noget Location-svar på dette ene kald, fordi v1 ikke har et opslag på en enkelt tidspost. Find den igen på GET /time-entries med matter_id og et datointerval.

Beløb er heltal i øre, også på vej ind

250000 er 2.500,00 kr. Det gælder hourly_rate, fixed_fee_amount, budget_amount og internal_rate.

En decimal er 422, og det gælder også 1250.00. Det er med vilje: en body der bærer kroner hvor kontrakten siger øre, er forkert med en faktor hundrede, og den fejl er billigere at få som en statuskode end at finde i en afstemning et halvt år senere.

Afstem bagefter

Kør kapitel 1 igen mod Levano når importen er kørt, og sammenlign med de tal du skrev ned. Fire tællinger der stemmer er den eneste kvittering der betyder noget.

Vær opmærksom på at et 200 med Idempotent-Replayed: true ikke er en oprettelse. Tæller dit script kun 2xx, tæller det for højt.

Tjekliste

  • Nøgle oprettet med hele læsesættet på én gang
  • Preflight kørt: syv lister svarer 200
  • Udtræk med statuskontrol i løkken, ikke en bar while
  • Antal pr. enhed holdt op mod kildesystemet
  • Referencer afstemt med comm, tom output
  • open_count fra ar-aging matcher fakturaudtrækket
  • Beløb gemt som heltal hele vejen ned i jeres egen database
  • Nøglekort bygget på parten, ikke på klienten
  • CPR-matchning planlagt som manuelt arbejde
  • Import-nøgle med parties:write, clients:write, matters:write, time:write
  • Idempotency-Key er kildesystemets række-id
  • Kapitel 1 kørt igen efter import, fire tællinger stemmer