# Gavekort

<https://kuvert.dk/no/dokumentation/api/giftcards>

Åtte endepunkter dekker hele kortets liv gjennom API'et: utsted, list, slå opp, innløs, omgjør, gjenfs, kanseller og refunder.

- `POST /v1/api/giftcards`
- `GET /v1/api/giftcards`
- `GET /v1/api/giftcards/{code}`
- `POST /v1/api/giftcards/{code}/redeem`
- `POST /v1/api/redemptions/{id}/reverse`
- `POST /v1/api/giftcards/{code}/resend`
- `POST /v1/api/giftcards/{code}/cancel`
- `POST /v1/api/orders/{id}/refund`

Kanseller kan ikke angres. Det finnes ingen reaktivering, fordi et kort som ble avsluttet og senere stille gjenopplivet, er en forpliktelse ingen har sagt ja til. Refunder er det eneste endepunktet som flytter penger: Stripe sender beløpet tilbake til kortet som betalte, og ingen andre steder.

## Prøv det her på siden

Konsollen herunder kjører de samme skjemaene og samme innløsningsregel som API'et, mot et kort som bare finnes i nettleseren din. Rett i kroppen, og se hvilken status ditt eget kall ville fått.

## Idempotens

Utsted og innløs kan begge ha en nøkkel som gjør kallet trygt å gjenta. Samme nøkkel to ganger svarer med resultatet fra den FØRSTE gangen (samme kort, samme postering) i stedet for å utstede eller trekke to ganger. De to verbene oppdager likevel ikke det samme: ved INNLØS svarer vi 409 idempotency_key_conflict hvis nøkkelen allerede er brukt på et annet kort eller på en utbetaling, i stedet for å treffe feil. Ved UTSTED sammenligner vi ikke kallet. Gjenbruker du en nøkkel med et annet beløp, får du 201 og det FØRSTE kortets data tilbake, uten feil. Bruk én nøkkel pr. kort.

Nøkkelen kan stå tre steder. Vi leser idempotencyKey i kroppen først, og ellers headeren Idempotency-Key og deretter X-Request-Guid. Kroppen vinner når det er både og.

| Hvor | Navn |
| --- | --- |
| Kroppen | idempotencyKey: det eksplisitte valget, og det som vinner. |
| Header | Idempotency-Key: navnet Stripe har gjort til standard. |
| Header | X-Request-Guid, så en integrasjon som flytter fra Lifepeaks, virker uten å skrives om. |

> **Viktig:** En header som er satt én gang for hele HTTP-klienten din, er ikke en nøkkel pr. kall. To innløsninger på SAMME kort med samme nøkkel gir 200 og den første innløsningens beløp tilbake. Det blir ikke trukket en gang til, og det kommer ingen feil. Gjesten betaler for kaffen med et kort som aldri ble trukket. Sett nøkkelen pr. kall, eller send den i kroppen.

## Utsted et kort

`POST /v1/api/giftcards`

Utsteder et kort utenom betalingsflyten, for eksempel solgt fra ditt eget system, fakturert til en bedrift eller gitt bort på huset.

`origin` er påkrevd her, hvor det er valgfritt i dashbordet. Et system utsteder uten at noen ser på, og valget følger kortet resten av dets levetid: `sold` betyr at en kunde har betalt for det, og så bærer kortet kundens rett til å få restbeløpet utbetalt kontant. `b2b` og `comp` gjør ikke.

> **Viktig:** Alle beløp er hele øre. Sender du 500 for å mene 500 DKKoner, utsteder du et kort på fem kroner.

### Kropp

- `amountOre` (`integer`, påkrevd): Kortets verdi i øre, minst 1 øre og høyst 100.000,00 DKK.
- `origin` (`"sold" | "b2b" | "comp"`, påkrevd): `sold`: en kunde har betalt. `b2b`: fakturert til en bedrift. `comp`: spandert av huset.
- `validityMonths` (`integer`, valgfri): Gyldighet i måneder, minst 36 og høyst 600. Utelates den, gjelder kortet i 36.
- `reference` (`string | null`, valgfri): Ditt eget notat, for eksempel et fakturanummer. Høyst 200 tegn.
- `noCashClaim` (`boolean`, valgfri): Merk kortet «kan ikke byttes inn i kontanter», for eksempel når det gjelder en bestemt ting. Det gjør ikke i seg selv en ansattgave skattefri: SKAT, den danske skattemyndigheten, regner digitale gavekort som skattepliktige. Bare på `b2b` og `comp`.
- `idempotencyKey` (`string`, valgfri): Samme nøkkel to ganger utsteder bare ett kort. Høyst 120 tegn. Kan også sendes som header (se Idempotens).

### Svarfelter

Kortet ligger under `giftCard`, i samme form som ved Slå et kort opp, der feltene er beskrevet.

### Statuskoder

- `201`: Kortet er utstedt. Med en idempotencyKey som er brukt før, er svaret kortet nøkkelen utstedte første gang.
- `400 invalid_issue_request`: Kroppen stemmer ikke med det utstedelse krever.
- `404 merchant_not_found`: Nøkkelen er gyldig, men bedriften bak den finnes ikke lenger. Det skjer sjelden og kan ikke rettes av en klient. Kontakt oss.
- `409 cash_claim_cannot_be_denied`: `noCashClaim` på et `sold`-kort. Kunden har betalt, og retten til å få restbeløpet utbetalt kan et flagg ikke fjerne.

### Forespørsel

```bash
curl -X POST https://api.kuvert.dk/v1/api/giftcards \
  -H "Authorization: Bearer $KUVERT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "amountOre": 50000,
    "origin": "b2b",
    "reference": "Faktura 2026-114",
    "idempotencyKey": "faktura-2026-114"
  }'
```

### Eksempel på svar · 201

```json
{
  "giftCard": {
    "id": "gc_4f1c9a2e7b3d48e6a0c5d9f2b8e1a7c3",
    "code": "KUV-7K9F-2XQD",
    "orderId": null,
    "status": "active",
    "initialOre": 50000,
    "balanceOre": 50000,
    "expiresAt": "2029-09-29T08:00:00.000Z",
    "merchantSlug": "cafe-noir",
    "origin": "b2b",
    "reference": "Faktura 2026-114",
    "punchValueOre": null,
    "noCashClaim": false,
    "productName": null
  }
}
```

## List kort

`GET /v1/api/giftcards`

Alle bedriftens kort, nyeste først, med saldoen regnet ut fra posteringene i samme øyeblikk. Listen er ikke sidedelt.

### Svarfelter

Kortene ligger under `giftCards`, hvert i samme form som ved Slå et kort opp.

### Statuskoder

- `200`: Listen, eventuelt tom.

### Forespørsel

```bash
curl https://api.kuvert.dk/v1/api/giftcards \
  -H "Authorization: Bearer $KUVERT_API_KEY"
```

### Eksempel på svar · 200

```json
{
  "giftCards": [
    {
      "id": "gc_4f1c9a2e7b3d48e6a0c5d9f2b8e1a7c3",
      "code": "KUV-7K9F-2XQD",
      "orderId": null,
      "status": "active",
      "initialOre": 50000,
      "balanceOre": 50000,
      "expiresAt": "2029-09-29T08:00:00.000Z",
      "merchantSlug": "cafe-noir",
      "origin": "b2b",
      "reference": "Faktura 2026-114",
      "punchValueOre": null,
      "noCashClaim": false,
      "productName": null
    },
    {
      "id": "gc_9b2e61d04c7f4a3e8d5b1c6a0f2e9d47",
      "code": "KUV-M4TQ-8WZR",
      "orderId": "ord_7d3f0b9c1e2a45f68b4c9d0e1f2a3b5c",
      "status": "active",
      "initialOre": 50000,
      "balanceOre": 43600,
      "expiresAt": "2029-06-12T14:31:07.000Z",
      "merchantSlug": "cafe-noir",
      "origin": "paid",
      "reference": null,
      "punchValueOre": null,
      "noCashClaim": false,
      "productName": "Gavekort"
    }
  ]
}
```

## Slå et kort opp

`GET /v1/api/giftcards/{code}`

Et kort ut fra koden, med saldoen slik den står akkurat nå.

> **Merk:** Du kan sende koden i den formen skanneren din gir deg: den fullstendige adressen fra QR-koden, koden uten bindestreker, små bokstaver. Kuvert normaliserer den. En billettkode (TKT-…) svarer 400 not_a_gift_card, fordi den hører til check-in, ikke til et gavekort. Det samme gjelder innløsning.

### Stiparametere

- `code` (`string`, påkrevd): Kortets kode, for eksempel `KUV-7K9F-2XQD`.

### Svarfelter

Kortet ligger under `giftCard`:

- `id` (`string`): Kortets id (`gc_…`). Det er koden du slår opp med.
- `code` (`string`): Koden som står på kortet.
- `orderId` (`string | null`): Ordren kortet kom fra. `null` for et kort utstedt for hånd eller gjennom API'et.
- `status` (`"active" | "redeemed" | "expired" | "void" | "disputed"`): Kortets tilstand. Bare `active` kan innløses.
- `initialOre` (`integer`): Kortets verdi i øre, før noe er innløst.
- `balanceOre` (`integer`): Saldoen i øre, regnet ut fra posteringene og ikke lest fra et felt.
- `expiresAt` (`string`): Når kortet utløper, ISO 8601 i UTC.
- `merchantSlug` (`string`): Butikkens adresse på shop.kuvert.dk.
- `origin` (`"paid" | "sold" | "b2b" | "corporate" | "comp"`): Hvor verdien kom fra. `paid` er kjøpt i butikken og `corporate` kjøpt som bedriftsgave; `sold`, `b2b` og `comp` er utstedt for hånd eller gjennom API'et.
- `reference` (`string | null`): Ditt eget notat fra utstedelsen. `null` på et kjøpt kort.
- `punchValueOre` (`integer | null`): Verdien av ett klipp hvis kortet er et klippekort, ellers `null`. Et klippekort løses inn i hele klipp.
- `noCashClaim` (`boolean`): Kortet kan ikke byttes inn i kontanter.
- `productName` (`string | null`): Produktets navn slik det lød da kortet ble utstedt. `null` på et kort utstedt for hånd.

### Statuskoder

- `200`: Kortet.
- `400 not_a_gift_card`: Koden i stien er en billettkode (TKT-…), ikke et gavekort.
- `404 gift_card_not_found`: Koden finnes ikke på din bedrift.

### Forespørsel

```bash
curl https://api.kuvert.dk/v1/api/giftcards/KUV-7K9F-2XQD \
  -H "Authorization: Bearer $KUVERT_API_KEY"
```

### Eksempel på svar · 200

```json
{
  "giftCard": {
    "id": "gc_4f1c9a2e7b3d48e6a0c5d9f2b8e1a7c3",
    "code": "KUV-7K9F-2XQD",
    "orderId": null,
    "status": "active",
    "initialOre": 50000,
    "balanceOre": 50000,
    "expiresAt": "2029-09-29T08:00:00.000Z",
    "merchantSlug": "cafe-noir",
    "origin": "b2b",
    "reference": "Faktura 2026-114",
    "punchValueOre": null,
    "noCashClaim": false,
    "productName": null
  }
}
```

## Innløs

`POST /v1/api/giftcards/{code}/redeem`

Trekker et beløp fra kortets saldo. Koden står i stien, beløpet i kroppen.

Gem `id`. Det er posteringen, og det er det eneste som kan angre nettopp denne innløsningen.

Går det ikke igjennom, er svaret én av disse. De er ikke like for kassen: noen er noe gjesten skal ha vite, og noen er noe bedriften skal ringes opp om.

> **Merk:** Send alltid en idempotencyKey fra en kasse. Timer nettet ut, og terminalen prøver igjen, sikrer nøkkelen at gjesten bare trekkes én gang.

> **Viktig:** Les statuskoden, ikke bare 200. En kasse som behandler alt annet enn 200 som «ukjent feil», kan ikke skille et utløpt kort fra et fryst kort eller fra et som bare trenger noen timer til. Det er tre ulike ting å si til gjesten.

### Stiparametere

- `code` (`string`, påkrevd): Kortets kode, for eksempel `KUV-7K9F-2XQD`.

### Kropp

- `amountOre` (`integer`, påkrevd): Beløpet i øre, minst 1. Et klippekort tar bare et helt antall klipp.
- `location` (`string | null`, valgfri): Hvor det skjedde, for eksempel «Vesterbro». Høyst 120 tegn.
- `idempotencyKey` (`string`, valgfri): Samme nøkkel to ganger trekker bare én gang. Høyst 120 tegn. Kan også sendes som header (se Idempotens).

### Svarfelter

Svaret er posteringen:

- `id` (`string`): Posteringens id (`red_…`), som du angrer den med.
- `code` (`string`): Kortets kode.
- `amountOre` (`integer`): Det trukne beløpet i øre.
- `remainingBalanceOre` (`integer`): Saldoen etter innløsningen.
- `status` (`"active" | "redeemed" | "expired" | "void" | "disputed"`): `status` er kortets tilstand etter innløsningen, en av `active`, `redeemed`, `expired`, `void`, `disputed`. Er saldoen brukt opp, skifter den til `redeemed`.

### Statuskoder

- `200`: Beløpet er trukket. Med en idempotencyKey som er brukt før på samme kort, er svaret det fra første gang, uten at det trekkes igjen.
- `400 invalid_redeem_request`: Kroppen stemmer ikke med det innløsning krever, typisk et beløp som ikke er et helt antall øre, eller som er null.
- `400 not_a_gift_card`: Koden i stien er en billettkode (TKT-…), ikke et gavekort.
- `404 gift_card_not_found`: Koden finnes ikke på din bedrift.
- `409 card_inactive`: Kortet er ikke aktivt: brukt opp, annullert, satt til utløpt eller fryst under en innsigelse. Det går ikke over av seg selv, så bedriften må kontaktes.
- `409 redeem_too_soon`: Bedriften har satt en innløsningsforsinkelse, og den er ikke utløpt ennå. Kortet feiler ingenting; det kan bare ikke brukes ennå. Prøv igjen senere.
- `409 idempotency_key_conflict`: Nøkkelen er allerede brukt på et annet kort eller på en utbetaling fra det samme kortet. Se Idempotens.
- `410 gift_card_expired`: Kortet står stadig som aktivt, men gyldighetssdatoen er passert. Til forskjell fra card_inactive er det tiden og ikke en beslutning som har lukket kortet.
- `422 insufficient_balance`: Beløpet er større enn saldoen. Vi trekker aldri et delbeløp på eget initiativ. Slå opp kortet, trekk saldoen, og ta resten på annen måte.
- `422 invalid_amount`: Innløsningsregelen sitt eget svar på et ulovlig beløp. Skjemaet fanger det normalt allerede som 400 invalid_redeem_request, så en klient bør kunne lese begge.
- `422 punch_amount_not_whole`: Kortet er et klippekort, og beløpet er ikke et helt antall klipp. Det er ikke noe galt med tallet, men det er feil type kort for det. Slå opp kortet, les punchValueOre, og send den verdien eller et multiplum av den. Et klippekort som står igjen med en rest under ett klipp, er penger verken kassen eller kunden kan nå.

### Forespørsel

```bash
curl -X POST https://api.kuvert.dk/v1/api/giftcards/KUV-7K9F-2XQD/redeem \
  -H "Authorization: Bearer $KUVERT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "amountOre": 6400,
    "location": "Vesterbro",
    "idempotencyKey": "pos-3-20260929-0142"
  }'
```

### Eksempel på svar · 200

```json
{
  "id": "red_8f21c0b45d3e4a9b8c7d6e5f4a3b2c1d",
  "code": "KUV-7K9F-2XQD",
  "amountOre": 6400,
  "remainingBalanceOre": 43600,
  "status": "active"
}
```

## Angre en innløsning

`POST /v1/api/redemptions/{id}/reverse`

Legger beløpet tilbake på kortet. Det skjer som en ny postering med negativt beløp, som peker på den den angrer. Ingenting slettes, og begge linjene blir stående i kortets historikk. Var kortet brukt helt opp, blir det aktivt igjen.

> **Merk:** Angringen utløser giftcard.redeemed som alle andre posteringer, bare med negativt beløp. Et system som speiler saldoer, må altså legge til beløpet, ikke ignorere hendelsen.

### Stiparametere

- `id` (`string`, påkrevd): Innløsningens id (`red_…`), som du fikk tilbake fra Innløs.

### Svarfelter

Svaret er posteringen som la beløpet tilbake, i samme form som ved Innløs, med negativt `amountOre`.

### Statuskoder

- `200`: Beløpet er lagt tilbake.
- `404 redemption_not_found`: Posteringen finnes ikke på din bedrift.
- `409 already_reversed`: Den er angret før. Én gang, aldri to.
- `409 not_reversible`: Posteringen er selv en angring eller en kontant utbetaling.
- `409 card_inactive`: Kortet er annullert eller fryst under en innsigelse.
- `409 order_refunded`: Ordren bak kortet er refundert. Kjøperen har allerede fått pengene tilbake, så en angring ville gi dem verdien to ganger.

### Forespørsel

```bash
curl -X POST https://api.kuvert.dk/v1/api/redemptions/red_8f21c0b45d3e4a9b8c7d6e5f4a3b2c1d/reverse \
  -H "Authorization: Bearer $KUVERT_API_KEY"
```

### Eksempel på svar · 200

```json
{
  "id": "red_2c77af90e1d24b3c9a8f7e6d5c4b3a21",
  "code": "KUV-7K9F-2XQD",
  "amountOre": -6400,
  "remainingBalanceOre": 50000,
  "status": "active"
}
```

## Send kortet igjen

`POST /v1/api/giftcards/{code}/resend`

Sender kortet til mottakeren på nytt. Det er det en kasse gjør når en kunde sier at e-posten aldri kom.

Uten `recipient` sender vi det kortets ordre ennå ikke har fått levert, til adressen som står på det. Et kort som allerede er levert, sendes ikke på nytt. Med `recipient` retter du adressen på akkurat dette kortet og sender det dit, også hvis det er levert før. Bare aktive kort sendes.

### Stiparametere

- `code` (`string`, påkrevd): Kortets kode, for eksempel `KUV-7K9F-2XQD`.

### Kropp

- `recipient` (`string`, valgfri): En ny e-postadresse til kortet. Kroppen kan utelates helt.

### Svarfelter

Svaret sier hvor mye som ble sendt:

- `delivered` (`integer`): Antall kort som ble sendt. 0 betyr at det ikke var noe å sende.

### Statuskoder

- `200`: Sendt, eller ingenting å sende.
- `400 not_a_gift_card`: Koden i stien er en billettkode (TKT-…), ikke et gavekort.
- `404 gift_card_not_found`: Koden finnes ikke på din bedrift.
- `404 order_not_found`: Gjenfs fant kortet, men ikke ordren det kom fra. Et kort utstedt i hånden har ingen ordre og kan derfor ikke gensendes på den måten.
- `409 card_has_no_delivery`: Kortet har aldri blitt sendt av oss, for det ble utlevert over disken. Det er ingen levering å gjenta.
- `422 invalid_recipient`: `recipient` i kroppen på gjenfs er ikke en e-postadresse vi kan sende til. Utelat feltet for å sende til den adressen kortet allerede står med.
- `502 delivery_failed`: Vi nådde leverandøren vår, og den avviste forsendelsen. Det er ingenting galt med kortet, og ingenting er trukket. Prøv igjen, eller kontroller adressen.

### Forespørsel

```bash
curl -X POST https://api.kuvert.dk/v1/api/giftcards/KUV-M4TQ-8WZR/resend \
  -H "Authorization: Bearer $KUVERT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "recipient": "ny-adresse@dit-domaene.dk"
  }'
```

### Eksempel på svar · 200

```json
{
  "delivered": 1
}
```

## Annuller et kort

`POST /v1/api/giftcards/{code}/cancel`

Annullerer kortet med en gang. Saldoen blir stående på kortet, men kan ikke lenger brukes, og det kan ikke angres. Er kortet allerede annullert, får du det tilbake slik det er, uten feil.

### Stiparametere

- `code` (`string`, påkrevd): Kortets kode, for eksempel `KUV-7K9F-2XQD`.

### Svarfelter

Kortet ligger under `giftCard`, nå med `status` `void`.

### Statuskoder

- `200`: Kortet er annullert, eller var det allerede.
- `400 not_a_gift_card`: Koden i stien er en billettkode (TKT-…), ikke et gavekort.
- `404 gift_card_not_found`: Koden finnes ikke på din bedrift.

### Forespørsel

```bash
curl -X POST https://api.kuvert.dk/v1/api/giftcards/KUV-7K9F-2XQD/cancel \
  -H "Authorization: Bearer $KUVERT_API_KEY"
```

### Eksempel på svar · 200

```json
{
  "giftCard": {
    "id": "gc_4f1c9a2e7b3d48e6a0c5d9f2b8e1a7c3",
    "code": "KUV-7K9F-2XQD",
    "orderId": null,
    "status": "void",
    "initialOre": 50000,
    "balanceOre": 50000,
    "expiresAt": "2029-09-29T08:00:00.000Z",
    "merchantSlug": "cafe-noir",
    "origin": "b2b",
    "reference": "Faktura 2026-114",
    "punchValueOre": null,
    "noCashClaim": false,
    "productName": null
  }
}
```

## Refunder en ordre

`POST /v1/api/orders/{id}/refund`

Sender hele betalingen for ordren tilbake til kortet som betalte, med kjøperens ekspedisjonsgebyr, og Kuverts gebyr på salget går tilbake til deg. Det er det eneste endepunktet som flytter penger.

Når Stripe har bekreftet refusjonen, tas verdien av ordrens kort, billettene annulleres, og order.refunded sendes. Det skjer som regel noen sekunder etter svaret.

Kallet tar ingen idempotensnøkkel, fordi ordrens egen status er sperren. En ordre som allerede er refundert, svarer 409 order_not_refundable.

### Stiparametere

- `id` (`string`, påkrevd): Ordrens id (`ord_…`): `orderId` på kortet eller i order.paid.

### Svarfelter

Svaret er refusjonen slik Stripe tok imot den:

- `orderId` (`string`): Ordren.
- `refundId` (`string`): Stripes id for refusjonen (`re_…`), som du finner den under i Stripe.
- `amountOre` (`integer`): Beløpet som sendes tilbake, i øre.

### Statuskoder

- `200`: Stripe har tatt imot refusjonen.
- `404 order_not_found`: Ordren finnes ikke hos bedriften din.
- `409 order_not_refundable`: Ordren kan ikke refunderes: den er ikke betalt, allerede refundert eller frosset under en innsigelse.
- `409 merchant_not_payment_ready`: Stripe-kontoen din er ikke koblet til.
- `503 stripe_not_configured`: Betalinger er midlertidig utilgjengelige hos oss. Prøv igjen senere.

### Forespørsel

```bash
curl -X POST https://api.kuvert.dk/v1/api/orders/ord_7d3f0b9c1e2a45f68b4c9d0e1f2a3b5c/refund \
  -H "Authorization: Bearer $KUVERT_API_KEY"
```

### Eksempel på svar · 200

```json
{
  "orderId": "ord_7d3f0b9c1e2a45f68b4c9d0e1f2a3b5c",
  "refundId": "re_3PqX9aLkT2mN8vRb1c4D7eFg",
  "amountOre": 50500
}
```
