# Gavekort

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

Otte endepunkter dækker hele kortets liv gennem API'et: udsted, list, slå op, indløs, fortryd, gensend, annullér og refundér.

- `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`

Annullér kan ikke fortrydes — der findes ingen genaktivering, fordi et kort, der blev slået ihjel og siden stille genoplivet, er en forpligtelse, ingen har sagt ja til. Refundér er det eneste endepunkt, der flytter penge: Stripe sender beløbet tilbage til det kort, der betalte, og ingen andre steder hen.

## Prøv det her på siden

Konsollen herunder kører de samme skemaer og den samme indløsningsregel som API'et, mod et kort, der kun findes i din browser. Ret i kroppen, og se hvilken status dit eget kald ville få.

## Idempotens

Udsted og indløs kan begge bære en nøgle, der gør kaldet sikkert at gentage. Samme nøgle to gange svarer med resultatet af den FØRSTE gang — samme kort, samme postering — i stedet for at udstede eller trække to gange. De to verber opdager dog ikke det samme: ved INDLØS svarer vi 409 idempotency_key_conflict, hvis nøglen allerede er brugt på et andet kort eller på en udbetaling, i stedet for at ramme forkert. Ved UDSTED sammenligner vi ikke kaldet — genbruger du en nøgle med et andet beløb, får du 201 og det FØRSTE korts data tilbage, uden fejl. Brug én nøgle pr. kort.

Nøglen kan stå tre steder. Vi læser idempotencyKey i kroppen først, og ellers headeren Idempotency-Key og derefter X-Request-Guid. Kroppen vinder, når der er både og.

| Hvor | Navn |
| --- | --- |
| Kroppen | idempotencyKey — det eksplicitte valg, og det, der vinder. |
| Header | Idempotency-Key — navnet Stripe har gjort til standard. |
| Header | X-Request-Guid — så en integration, der flytter fra Lifepeaks, virker uden at blive skrevet om. |

> **Vigtigt:** En header, der er sat én gang for hele din HTTP-klient, er ikke en nøgle pr. kald. To indløsninger på SAMME kort med samme nøgle giver 200 og den første indløsnings beløb tilbage — der bliver ikke trukket anden gang, og der kommer ingen fejl. Gæsten betaler for kaffen med et kort, der aldrig blev trukket. Sæt nøglen pr. kald, eller send den i kroppen.

## Udsted et kort

`POST /v1/api/giftcards`

Udsteder et kort uden om betalingsflowet — solgt fra dit eget system, faktureret til en virksomhed, eller givet med på huset.

`origin` er påkrævet her, hvor det er valgfrit i dashboardet. Et system udsteder uden nogen kigger med, og valget følger kortet resten af dets levetid: `sold` betyder, at en kunde har betalt for det, og så bærer kortet kundens ret til at få restbeløbet udbetalt kontant. `b2b` og `comp` gør ikke.

> **Vigtigt:** Alle beløb er heltal i øre. Sender du 500 for at mene 500 kroner, udsteder du et kort på fem kroner.

### Krop

- `amountOre` (`integer`, påkrævet): Kortets værdi i øre — mindst 1 øre og højst 100.000,00 kr.
- `origin` (`"sold" | "b2b" | "comp"`, påkrævet): `sold`: en kunde har betalt. `b2b`: faktureret til en virksomhed. `comp`: givet med på huset.
- `validityMonths` (`integer`, valgfri): Gyldighed i måneder, mindst 36 og højst 600. Udelades den, gælder kortet i 36.
- `reference` (`string | null`, valgfri): Din egen note, fx et fakturanummer. Højst 200 tegn.
- `noCashClaim` (`boolean`, valgfri): Markér kortet «kan ikke ombyttes til kontanter» — det, der gør en medarbejdergave skattefri. Kun på `b2b` og `comp`.
- `idempotencyKey` (`string`, valgfri): Samme nøgle to gange udsteder kun ét kort. Højst 120 tegn. Kan også sendes som header — se Idempotens.

### Svarfelter

Kortet står under `giftCard`, i samme form som ved Slå et kort op, hvor felterne er beskrevet.

### Statuskoder

- `201`: Kortet er udstedt — eller, med en idempotencyKey, der er brugt før, det kort, nøglen udstedte første gang.
- `400 invalid_issue_request`: Kroppen matcher ikke det, udstedelse kræver.
- `404 merchant_not_found`: Nøglen er gyldig, men forretningen bag den findes ikke længere. Sjældent, og ikke noget en klient kan rette — kontakt os.
- `409 cash_claim_cannot_be_denied`: `noCashClaim` på et `sold`-kort. Kunden har betalt, og retten til at få restbeløbet udbetalt kan et flag ikke fjerne.

### Forespørgsel

```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 forretningens kort, nyeste først, med saldoen regnet ud af posteringerne i samme øjeblik. Listen er ikke sidedelt.

### Svarfelter

Kortene står under `giftCards`, hvert i samme form som ved Slå et kort op.

### Statuskoder

- `200`: Listen, eventuelt tom.

### Forespørgsel

```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 op

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

Et kort ud fra dets kode, med saldoen, som den står lige nu.

> **Bemærk:** Du kan sende koden i den form, din scanner giver dig: den fulde adresse fra QR-koden, koden uden bindestreger, små bogstaver. Kuvert normaliserer den. En billetkode (TKT-…) svarer 400 not_a_gift_card — den hører til check-in, ikke til et gavekort. Det samme gælder indløsning.

### Stiparametre

- `code` (`string`, påkrævet): Kortets kode, fx `KUV-7K9F-2XQD`.

### Svarfelter

Kortet står under `giftCard`:

- `id` (`string`): Kortets id (`gc_…`). Koden er det, du slår op med.
- `code` (`string`): Koden, der står på kortet.
- `orderId` (`string | null`): Ordren, kortet kom fra. `null` for et kort udstedt i hånden eller gennem API’et.
- `status` (`"active" | "redeemed" | "expired" | "void" | "disputed"`): Kortets tilstand. Kun `active` kan indløses.
- `initialOre` (`integer`): Kortets værdi i øre, før noget er indløst.
- `balanceOre` (`integer`): Saldoen i øre — regnet ud af posteringerne, ikke læst af et felt.
- `expiresAt` (`string`): Hvornår kortet udløber, ISO 8601 i UTC.
- `merchantSlug` (`string`): Butikkens adresse på shop.kuvert.dk.
- `origin` (`"paid" | "sold" | "b2b" | "corporate" | "comp"`): Hvor værdien kom fra. `paid` er købt i butikken og `corporate` købt som erhvervsgave; `sold`, `b2b` og `comp` er udstedt i hånden eller gennem API’et.
- `reference` (`string | null`): Din egen note fra udstedelsen. `null` på et købt kort.
- `punchValueOre` (`integer | null`): Værdien af ét klip, hvis kortet er et klippekort — ellers `null`. Et klippekort indløses i hele klip.
- `noCashClaim` (`boolean`): Kortet kan ikke ombyttes til kontanter.
- `productName` (`string | null`): Produktets navn, som det lød, da kortet blev udstedt. `null` på et kort udstedt i hånden.

### Statuskoder

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

### Forespørgsel

```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
  }
}
```

## Indløs

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

Trækker et beløb fra kortets saldo. Koden står i stien, beløbet i kroppen.

Gem `id`. Det er posteringen, og det er det eneste, der kan fortryde netop denne indløsning.

Går det ikke igennem, er svaret én af disse. De er ikke ens for kassen: nogle er noget, gæsten skal have at vide, og nogle er noget, forretningen skal ringes op om.

> **Bemærk:** Send altid en idempotencyKey fra en kasse. Timer nettet ud, og terminalen prøver igen, sikrer nøglen, at gæsten kun trækkes én gang.

> **Vigtigt:** Læs statuskoden, ikke kun 200. En kasse, der behandler alt andet end 200 som «ukendt fejl», kan ikke skelne et udløbet kort fra et frosset fra et, der bare skal have et par timer mere — og det er tre forskellige ting at sige til gæsten.

### Stiparametre

- `code` (`string`, påkrævet): Kortets kode, fx `KUV-7K9F-2XQD`.

### Krop

- `amountOre` (`integer`, påkrævet): Beløbet i øre, mindst 1. Et klippekort tager kun et helt antal klip.
- `location` (`string | null`, valgfri): Hvor det skete, fx «Vesterbro». Højst 120 tegn.
- `idempotencyKey` (`string`, valgfri): Samme nøgle to gange trækker kun én gang. Højst 120 tegn. Kan også sendes som header — se Idempotens.

### Svarfelter

Svaret er posteringen:

- `id` (`string`): Posteringens id (`red_…`) — det, du fortryder den med.
- `code` (`string`): Kortets kode.
- `amountOre` (`integer`): Det trukne beløb i øre.
- `remainingBalanceOre` (`integer`): Saldoen efter indløsningen.
- `status` (`"active" | "redeemed" | "expired" | "void" | "disputed"`): `status` er kortets tilstand efter indløsningen — en af `active`, `redeemed`, `expired`, `void`, `disputed`. Er saldoen brugt op, skifter den til `redeemed`.

### Statuskoder

- `200`: Beløbet er trukket — eller, med en idempotencyKey brugt før på samme kort, svaret fra første gang, uden at der trækkes igen.
- `400 invalid_redeem_request`: Kroppen matcher ikke det, indløsning kræver — typisk et beløb, der ikke er et helt antal øre, eller som er nul.
- `400 not_a_gift_card`: Koden i stien er en billetkode (TKT-…), ikke et gavekort.
- `404 gift_card_not_found`: Koden findes ikke på din forretning.
- `409 card_inactive`: Kortet er ikke aktivt: brugt op, annulleret, sat til udløbet eller frosset under en indsigelse. Det går ikke over af sig selv — forretningen skal kontaktes.
- `409 redeem_too_soon`: Forretningen har sat en indløsningsforsinkelse, og den er ikke udløbet endnu. Kortet fejler ingenting; det kan bare ikke bruges endnu. Prøv igen senere.
- `409 idempotency_key_conflict`: Nøglen er allerede brugt på et andet kort — eller på en udbetaling af det samme kort. Se Idempotens.
- `410 gift_card_expired`: Kortet står stadig som aktivt, men gyldighedsdatoen er passeret. Til forskel fra card_inactive er det tiden og ikke en beslutning, der har lukket kortet.
- `422 insufficient_balance`: Beløbet er større end saldoen. Vi trækker aldrig et delvist beløb af os selv — slå kortet op, træk saldoen, og tag resten på anden vis.
- `422 invalid_amount`: Indløsningsreglens eget svar på et ulovligt beløb. Skemaet fanger det normalt allerede som 400 invalid_redeem_request, så en klient bør kunne læse begge.
- `422 punch_amount_not_whole`: Kortet er et klippekort, og beløbet er ikke et helt antal klip. Der er ikke noget galt med tallet — det er den forkerte slags kort til det. Slå kortet op, læs punchValueOre, og send den værdi eller et multiplum af den. Et klippekort, der står tilbage med en rest under ét klip, er penge, hverken kassen eller kunden kan nå.

### Forespørgsel

```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"
}
```

## Fortryd en indløsning

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

Lægger beløbet tilbage på kortet. Det sker som en ny postering med negativt beløb, der peger på den, den fortryder — ingenting slettes, og begge linjer bliver stående i kortets historik. Var kortet brugt helt op, bliver det aktivt igen.

> **Bemærk:** Fortrydelsen udløser giftcard.redeemed som enhver anden postering — med negativt beløb. Et system, der spejler saldi, skal altså lægge beløbet til, ikke ignorere hændelsen.

### Stiparametre

- `id` (`string`, påkrævet): Indløsningens id (`red_…`), som du fik tilbage fra Indløs.

### Svarfelter

Svaret er posteringen, der lagde beløbet tilbage — i samme form som ved Indløs, med negativt `amountOre`.

### Statuskoder

- `200`: Beløbet er lagt tilbage.
- `404 redemption_not_found`: Posteringen findes ikke på din forretning.
- `409 already_reversed`: Den er fortrudt før. Én gang, aldrig to.
- `409 not_reversible`: Posteringen er selv en fortrydelse eller en kontant udbetaling.
- `409 card_inactive`: Kortet er annulleret eller frosset under en indsigelse.
- `409 order_refunded`: Ordren bag kortet er refunderet. Køberen har allerede fået pengene tilbage, så en fortrydelse ville give dem værdien to gange.

### Forespørgsel

```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 igen

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

Sender kortet til modtageren igen — det, en kasse gør, når en kunde siger, at mailen aldrig kom.

Uden `recipient` sender vi det, kortets ordre endnu ikke har fået leveret, til den adresse, det står med — et kort, der allerede er leveret, sendes ikke igen. Med `recipient` retter du adressen på netop dette kort og sender det dertil, også hvis det er leveret før. Kun aktive kort sendes.

### Stiparametre

- `code` (`string`, påkrævet): Kortets kode, fx `KUV-7K9F-2XQD`.

### Krop

- `recipient` (`string`, valgfri): En ny emailadresse til kortet. Kroppen kan udelades helt.

### Svarfelter

Svaret siger, hvor meget der blev sendt:

- `delivered` (`integer`): Antal kort, der blev sendt. 0 betyder, at der ikke var noget at sende.

### Statuskoder

- `200`: Sendt — eller intet at sende.
- `400 not_a_gift_card`: Koden i stien er en billetkode (TKT-…), ikke et gavekort.
- `404 gift_card_not_found`: Koden findes ikke på din forretning.
- `404 order_not_found`: Gensend fandt kortet, men ikke den ordre, det kom fra. Et kort udstedt i hånden har ingen ordre og kan derfor ikke gensendes ad den vej.
- `409 card_has_no_delivery`: Kortet har aldrig været sendt af os — det blev udleveret over disken. Der er ingen levering at gentage.
- `422 invalid_recipient`: `recipient` i kroppen på gensend er ikke en emailadresse, vi kan sende til. Udelad feltet for at sende til den adresse, kortet allerede står med.
- `502 delivery_failed`: Vi nåede vores udbyder, og den afviste forsendelsen. Kortet fejler ingenting, og intet er trukket — prøv igen, eller kontrollér adressen.

### Forespørgsel

```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
}
```

## Annullér et kort

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

Annullerer kortet med det samme. Saldoen bliver stående på kortet, men den kan ikke længere bruges, og det kan ikke fortrydes. Er kortet allerede annulleret, får du det tilbage, som det er, uden fejl.

### Stiparametre

- `code` (`string`, påkrævet): Kortets kode, fx `KUV-7K9F-2XQD`.

### Svarfelter

Kortet står under `giftCard`, nu med `status` `void`.

### Statuskoder

- `200`: Kortet er annulleret — eller var det allerede.
- `400 not_a_gift_card`: Koden i stien er en billetkode (TKT-…), ikke et gavekort.
- `404 gift_card_not_found`: Koden findes ikke på din forretning.

### Forespørgsel

```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
  }
}
```

## Refundér en ordre

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

Sender hele betalingen for ordren tilbage til det kort, der betalte — køberens ekspeditionsgebyr med — og Kuverts gebyr på salget går tilbage til dig. Det er det eneste endepunkt, der flytter penge.

Når Stripe har bekræftet refunderingen, tages værdien af ordrens kort, dens billetter annulleres, og order.refunded sendes. Det sker typisk få sekunder efter svaret.

Kaldet tager ingen idempotensnøgle — ordrens egen status er spærren. En ordre, der allerede er refunderet, svarer 409 order_not_refundable.

### Stiparametre

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

### Svarfelter

Svaret er refunderingen, som Stripe tog imod den:

- `orderId` (`string`): Ordren.
- `refundId` (`string`): Stripes id for refunderingen (`re_…`) — det, du finder den under i Stripe.
- `amountOre` (`integer`): Beløbet, der sendes tilbage, i øre.

### Statuskoder

- `200`: Stripe har taget imod refunderingen.
- `404 order_not_found`: Ordren findes ikke på din forretning.
- `409 order_not_refundable`: Ordren kan ikke refunderes: den er ikke betalt, allerede refunderet, eller frosset under en indsigelse.
- `409 merchant_not_payment_ready`: Din Stripe-konto er ikke forbundet.
- `503 stripe_not_configured`: Betalinger er midlertidigt utilgængelige hos os. Prøv igen senere.

### Forespørgsel

```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
}
```
