Formål og præcis afgrænsning
Operationen gemmer en salgsfakturakladde gennem Hours' eksisterende funktion til salgsfakturaer. Appen indsender fakturadato, kundenavn og linjer, og Hours returnerer kladdens id og status: "draft". Fakturaen gemmes med sent: false og posted: false, og total_incl_vat_minor er null, indtil fakturaen er bogført. Der tildeles ikke automatisk et endeligt fakturanummer i dette kald.
Der sendes ingen faktura til modtageren, oprettes ingen Stripe-betaling, gennemføres ingen betaling og bogføres ingen endelig salgsfaktura af denne operation. Udstedelse, afsendelse, bogføring og betaling er forskellige trin og må ikke samles i en klientmeddelelse som »fakturaen er betalt«.
Før kaldet
Vælg den eksplicit konfigurerede installation. HOURS_BASE er enten dens /api/partner/sandbox/v1 eller /api/partner/v1. Basisadressen vises i App Console for appen. Et sandboxtoken begynder med hsk_; et produktions-access-token begynder med hat_. De er ikke indbyrdes udskiftelige og er ikke Supabase-login-tokens.
Produktion kræver en godkendt apprevision, en kundegrant og de relevante scopes. Kundegrantet er givet af organisationsejeren med tofaktor (aal2), gælder de valgte konti og varer højst 90 dage. Endelig journalbogføring kræver et særskilt mandat. Sandbox kræver en klargjort appgeneration i et separat projekt. En gyldig tokenform alene er ikke adgang.
Input og grænser
| Felt | Type | Påkrævet | Regler |
|---|---|---|---|
customer_name | string | Ja | minLength=1; maxLength=200 |
issue_date | string | Ja | date |
due_date | nullable | Nej | null eller string; date |
delivery_date | nullable | Nej | null eller string; date |
customer_address | nullable | Nej | null eller string |
currency | string | Nej | const="DKK"; default="DKK" |
notes | nullable | Nej | null eller string; minLength=1; maxLength=2000 |
lines | array | Ja | minItems=1; maxItems=500 |
Linjefelter
| Felt | Type | Påkrævet | Regler |
|---|---|---|---|
lines[].description | string | Ja | minLength=1; maxLength=1000 |
lines[].qty | number | Nej | exclusiveMinimum=0; maximum=1000000; multipleOf=0.001; default=1 |
lines[].unit | string | Nej | minLength=1; maxLength=30; default="stk." |
lines[].unit_price_minor | integer | Ja | minimum=0; maximum=99999999999999 |
lines[].discount_pct | number | Nej | minimum=0; exclusiveMaximum=100; multipleOf=0.001; default=0 |
lines[].tax_code_id | nullable | Nej | null eller string; uuid |
lines[].income_account_no | string | Ja | minLength=1; maxLength=30 |
Ukendte felter afvises. Hele body er højst 256 KiB, datoer skal være gyldige kalenderdatoer, og beløb er heltal i hele øre, højst 99 999 999 999 999 pr. linje. Valgfrie null-værdier og defaults normaliseres af serverens inputmodel. Organisation, aktør, intern kilde og bankkobling bestemmes ikke af payloaden.
Eksempel
Gem følgende syntetiske input som input.json. UUID'er erstattes med referencer fra den konkrete sandbox eller kundeautorisation; de må ikke genbruges mellem miljøer.
{
"customer_name": "Syntetisk kunde",
"issue_date": "2026-09-30",
"due_date": "2026-10-30",
"delivery_date": "2026-09-30",
"customer_address": "Eksempelvej 1, 1000 København K",
"currency": "DKK",
"lines": [
{
"description": "Rådgivning",
"qty": 1.5,
"unit": "time",
"unit_price_minor": 80000,
"income_account_no": "1000",
"discount_pct": 0
}
]
}curl --silent --show-error --fail-with-body \
--request POST "$HOURS_BASE/invoices" \
--header "Authorization: Bearer $HOURS_TOKEN" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: $IDEMPOTENCY_KEY" \
--data-binary @input.jsonSucces og output
Forvent HTTP 201 ved succes:
{
"id": "00000000-0000-4000-8000-000000000300",
"status": "draft",
"sent": false,
"posted": false,
"total_incl_vat_minor": null,
"operation_id": "00000000-0000-4000-8000-000000000900",
"replayed": false,
"environment": "sandbox"
}Et identisk replay giver samme successtatus og sætter replayed til true. Replay-svaret udelader environment, så klienten må ikke kræve feltet på replay. Produktions- og sandboxkontekst skal allerede være kendt fra kaldet, ikke udledes af en eventuelt manglende kvitteringsetiket.
Fejl og genforsøg
Manglende eller forkert miljøtoken afvises. Manglende scope, tilbagekaldt grant eller ændret apprevision stopper kaldet. Reference- og domænefejl skal afklares i stedet for at blive omskrevet til tilfældige gyldige værdier.
Ved timeout med ukendt udfald genbruges samme idempotensnøgle og samme input. En ændret payload under samme nøgle giver konflikt. En ny grant kan også give idempotency_grant_changed. Input, der afvises før regnskabskernen (invalid_input og invalid_amount), gemmer ingen kvittering, så samme nøgle kan genbruges med rettet input. Klientens forretningslog skal derfor gemme den oprindelige kontekst.
Sådan læses fakturalinjerne
I eksemplet er qty: 1.5 halvanden time, og unit_price_minor: 80000 er en enhedspris på 800,00 DKK. Linjens bruttobeløb før rabat og moms er derfor 120.000 øre. Udelades qty, er antallet 1. Antal og rabat accepterer højst tre decimaler; pris er altid et helt ørebeløb.
Gatewayen kontrollerer også den samlede bruttopris før rabat og moms med heltalsberegning i tusindedele. To hver for sig gyldige linjer kan derfor samlet blive afvist, hvis summen overskrider grænsen på 99 999 999 999 999 øre pr. linje. Det er en indgangsgrænse, ikke en påstand om den efterfølgende momsberegning eller endelige fakturatotal.
income_account_no er et kontonummer og skal kunne matches til en konto, kunden har tilladt. tax_code_id er en UUID-reference og valideres inden for samme organisation. Et manglende moms-ID må ikke udfyldes med et vilkårligt ID fra en anden virksomhed for at få kaldet til at lykkes.
Fakturakladden bruger kundenavn, eventuel kundeadresse og eventuel leveringsdato fra input. Den opretter ikke automatisk en modpart, en kundekonto i Hours eller en maillevering. Et blankt navn, en ugyldig dato eller en forfaldsdato før fakturadatoen skal rettes ved kilden.
Test din integration
Test korrekt kontekst, manglende scope, forkert miljø og fremmede referencer. Test også to samtidige identiske requests, ændret payload med samme nøgle og tabt svar efter commit. Kontrollér, at der kun er oprettet én kladde; et 201-svar fra en simuleret server er ikke bevis herpå.
Se rettigheder, fejl og idempotens.