# Presentkort

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

Åtta slutpunkter täcker hela kortets liv genom API:et: utfärda, lista, slå upp, lös in, ångra, omskicka, annullera och återbetala.

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

Annullera kan inte ångras. Det finns ingen återaktivering, eftersom ett kort som dödades och sedan tyst väcktes till liv igen är en skuld som ingen har sagt ja till. Återbetala är den enda slutpunkten som flyttar pengar: Stripe skickar tillbaka beloppet till kortet som betalade, och ingen annanstans.

## Prova det här på sidan

Konsolen nedan kör samma scheman och samma inlösningsregel som API:t, mot ett kort som bara finns i din browser. Redigera bodyn, och se vilken status ditt eget anrop skulle få.

## Idempotens

Utfärda och lösa in kan båda ha en nyckel som gör anropet säkert att upprepa. Samma nyckel två gånger svarar med resultatet från den FÖRSTA gången (samma kort, samma post) i stället för att utfärda eller dra två gånger. De två verben upptäcker dock inte samma sak: vid LÖSA IN svarar vi 409 idempotency_key_conflict om nyckeln redan har använts på ett annat kort eller på en utbetalning, i stället för att träffa fel. Vid UTFÄRDA jämför vi inte anropet. Återanvänder du en nyckel med ett annat belopp får du 201 och det FÖRSTA kortets data tillbaka, utan fel. Använd en nyckel per kort.

Nyckeln kan stå på tre platser. Vi läser idempotencyKey i bodyn först, och annars headern Idempotency-Key och sedan X-Request-Guid. Bodyn vinner, när det finns både och.

| Var | Namn |
| --- | --- |
| Bodyn | idempotencyKey: det uttryckliga valet, och det som vinner. |
| Header | Idempotency-Key: namnet som Stripe har gjort till standard. |
| Header | X-Request-Guid, så att en integration som flyttar från Lifepeaks fungerar utan att skrivas om. |

> **Viktigt:** En header som sätts en gång för hela din HTTP-klient är ingen nyckel per anrop. Två inlösningar på SAMMA kort med samma nyckel ger 200 och den första inlösningens belopp tillbaka. Det dras inte en andra gång, och det kommer inget fel. Gästen betalar för kaffet med ett kort som aldrig debiterades. Sätt nyckeln per anrop, eller skicka den i bodyn.

## Utfärda ett kort

`POST /v1/api/giftcards`

Utfärdar ett kort utanför betalningsflödet, t.ex. sålt från ditt eget system, fakturerat till ett företag eller bjudet på av huset.

`origin` är obligatorisk här, där det är valfritt i kontrollpanelen. Ett system utfärdar utan att någon tittar, och valet följer kortet resten av dess livslängd: `sold` betyder att en kund har betalat för det, och då bär kortet kundens rätt att få restbeloppet utbetalt kontant. `b2b` och `comp` gör inte.

> **Viktigt:** Alla belopp är heltal i öre. Skickar du 500 för att mena 500 Dkronor, utfärdar du ett kort på fem kronor.

### Body

- `amountOre` (`integer`, obligatorisk): Kortets värde i öre, minst 1 öre och högst 100 000,00 Dkr.
- `origin` (`"sold" | "b2b" | "comp"`, obligatorisk): `sold`: en kund har betalat. `b2b`: fakturerat till ett företag. `comp`: bjudet på av huset.
- `validityMonths` (`integer`, valfri): Giltighet i månader, minst 36 och högst 600. Utelämnas den gäller kortet i 36.
- `reference` (`string | null`, valfri): Din egen anteckning, t.ex. ett fakturanummer. Högst 200 tecken.
- `noCashClaim` (`boolean`, valfri): Markera kortet «kan inte växlas in mot kontanter», till exempel när det gäller en bestämd sak. Det gör inte i sig en personalgåva skattefri: den danska skattemyndigheten SKAT räknar digitala presentkort som skattepliktiga. Bara på `b2b` och `comp`.
- `idempotencyKey` (`string`, valfri): Samma nyckel två gånger utfärdar bara ett kort. Högst 120 tecken. Kan också skickas som header (se Idempotens).

### Svarsfält

Kortet ligger under `giftCard`, i samma form som vid Slå upp ett kort, där fälten beskrivs.

### Statuskoder

- `201`: Kortet är utfärdat. Med en idempotencyKey som har använts tidigare är svaret det kort som nyckeln utfärdade första gången.
- `400 invalid_issue_request`: Kroppen matchar inte det som utfärdning kräver.
- `404 merchant_not_found`: Nyckeln är giltig, men företaget bakom den finns inte längre. Det händer sällan och kan inte rättas av en klient. Kontakta oss.
- `409 cash_claim_cannot_be_denied`: `noCashClaim` på ett `sold`-kort. Kunden har betalat, och rätten att få restbeloppet utbetalt kan en flagga inte ta bort.

### Anrop

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

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

## Lista kort

`GET /v1/api/giftcards`

Alla företagets kort, nyaste först, med saldot uträknat ur transaktionerna i samma ögonblick. Listan är inte sidindelad.

### Svarsfält

Korten ligger under `giftCards`, vart och ett i samma form som vid Slå upp ett kort.

### Statuskoder

- `200`: Listan, eventuellt tom.

### Anrop

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

### Exempel 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å upp ett kort

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

Ett kort utifrån dess kod, med saldot som det står just nu.

> **Anmärkning:** Du kan skicka koden i den form som din skanner ger dig: hela adressen från QR-koden, koden utan bindestreck, små bokstäver. Kuvert normaliserar den. En biljettkod (TKT-…) svarar 400 not_a_gift_card, eftersom den hör till incheckning, inte till ett presentkort. Detsamma gäller inlösen.

### Sökvägsparametrar

- `code` (`string`, obligatorisk): Kortets kod, t.ex. `KUV-7K9F-2XQD`.

### Svarsfält

Kortet ligger under `giftCard`:

- `id` (`string`): Kortets id (`gc_…`). Koden är det du slår upp med.
- `code` (`string`): Koden som står på kortet.
- `orderId` (`string | null`): Ordern som kortet kom från. `null` för ett kort som utfärdats för hand eller via API:t.
- `status` (`"active" | "redeemed" | "expired" | "void" | "disputed"`): Kortets tillstånd. Bara `active` kan lösas in.
- `initialOre` (`integer`): Kortets värde i öre, innan något lösts in.
- `balanceOre` (`integer`): Saldot i öre, uträknat ur posterna och inte läst från ett fält.
- `expiresAt` (`string`): När kortet löper ut, ISO 8601 i UTC.
- `merchantSlug` (`string`): Butikens adress på shop.kuvert.dk.
- `origin` (`"paid" | "sold" | "b2b" | "corporate" | "comp"`): Var värdet kom ifrån. `paid` är köpt i butiken och `corporate` köpt som företagsgåva; `sold`, `b2b` och `comp` är utfärdade för hand eller via API:t.
- `reference` (`string | null`): Din egen anteckning från utfärdandet. `null` på ett köpt kort.
- `punchValueOre` (`integer | null`): Värdet av ett klipp om kortet är ett klippkort, annars `null`. Ett klippkort löses in i hela klipp.
- `noCashClaim` (`boolean`): Kortet kan inte växlas in mot kontanter.
- `productName` (`string | null`): Produktens namn som det löd när kortet utfärdades. `null` på ett kort som utfärdats för hand.

### Statuskoder

- `200`: Kortet.
- `400 not_a_gift_card`: Koden i sökvägen är en biljettskod (TKT-…), inte ett presentkort.
- `404 gift_card_not_found`: Koden finns inte på ditt företag.

### Anrop

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

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

## Lösa in

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

Drar ett belopp från kortets saldo. Koden står i sökvägen, beloppet i bodyn.

Spara `id`. Det är posteringen, och det är det enda som kan ångra just denna inlösning.

Om det inte går igenom, är svaret en av dessa. De är inte samma för kassan: några är något gästen bör veta, och några är något företaget bör ringas om.

> **Anmärkning:** Skicka alltid en idempotencyKey från ett kassaregister. Timer nätverket, och terminalen försöker igen, nyckeln säkerställer att gästen bara dras en gång.

> **Viktigt:** Läs statuskoden, inte bara 200. En kassa som behandlar allt annat än 200 som «okänt fel» kan inte skilja ett utgånget kort från ett fryst eller från ett som bara behöver några timmar till. Det är tre olika saker att säga till gästen.

### Sökvägsparametrar

- `code` (`string`, obligatorisk): Kortets kod, t.ex. `KUV-7K9F-2XQD`.

### Body

- `amountOre` (`integer`, obligatorisk): Beloppet i öre, minst 1. Ett klippkort tar bara ett helt antal klipp.
- `location` (`string | null`, valfri): Var det skedde, t.ex. «Vesterbro». Högst 120 tecken.
- `idempotencyKey` (`string`, valfri): Samma nyckel två gånger drar bara en gång. Högst 120 tecken. Kan också skickas som header (se Idempotens).

### Svarsfält

Svaret är transaktionen:

- `id` (`string`): Postens id (`red_…`), som du ångrar den med.
- `code` (`string`): Kortets kod.
- `amountOre` (`integer`): Det dragna beloppet i öre.
- `remainingBalanceOre` (`integer`): Saldot efter inlösningen.
- `status` (`"active" | "redeemed" | "expired" | "void" | "disputed"`): `status` är kortets status efter inlösen, en av `active`, `redeemed`, `expired`, `void`, `disputed`. Är saldot förbrukat ändras den till `redeemed`.

### Statuskoder

- `200`: Beloppet är draget. Med en idempotencyKey som har använts tidigare på samma kort är svaret det från första gången, utan att något dras igen.
- `400 invalid_redeem_request`: Bodyn matchar inte det som inlösen kräver, vanligtvis ett belopp som inte är ett helt antal öre eller som är noll.
- `400 not_a_gift_card`: Koden i sökvägen är en biljettskod (TKT-…), inte ett presentkort.
- `404 gift_card_not_found`: Koden finns inte på ditt företag.
- `409 card_inactive`: Kortet är inte aktivt: förbrukat, annullerat, satt till utgånget eller fryst under en tvist. Det går inte över av sig självt, så företaget behöver kontaktas.
- `409 redeem_too_soon`: Företaget har satt en inlösningsfördröjning, och den är inte utgångsen än. Kortet har inget fel; det kan bara inte användas än. Försök igen senare.
- `409 idempotency_key_conflict`: Nyckeln har redan använts på ett annat kort eller på en utbetalning från samma kort. Se Idempotens.
- `410 gift_card_expired`: Kortet står fortfarande som aktivt, men utgångsdatumet har passerat. Till skillnad från card_inactive är det tiden och inte ett beslut som har stängt kortet.
- `422 insufficient_balance`: Beloppet är större än saldot. Vi drar aldrig ett delbelopp på eget initiativ. Slå upp kortet, dra saldot och ta resten på annat sätt.
- `422 invalid_amount`: Inlösningsregelns eget svar på ett olagligt belopp. Schemat fångar det normalt redan som 400 invalid_redeem_request, så en klient bör kunna läsa båda.
- `422 punch_amount_not_whole`: Kortet är ett klippkort, och beloppet är inte ett helt antal klipp. Det är inget fel på talet, men det är fel sorts kort för det. Slå upp kortet, läs punchValueOre och skicka det värdet eller en multipel av det. Ett klippkort som blir kvar med en rest under ett klipp är pengar som varken kassan eller kunden kan komma åt.

### Anrop

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

### Exempel på svar · 200

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

## Ångra en inlösning

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

Lägger tillbaka beloppet på kortet. Det sker som en ny post med negativt belopp som pekar på den post den ångrar. Ingenting raderas, och båda raderna står kvar i kortets historik. Var kortet helt förbrukat blir det aktivt igen.

> **Anmärkning:** Ångringen utlöser giftcard.redeemed som vilken annan post som helst, bara med negativt belopp. Ett system som speglar saldon ska alltså lägga till beloppet, inte ignorera händelsen.

### Sökvägsparametrar

- `id` (`string`, obligatorisk): Inlösningens id (`red_…`), som du fick tillbaka från Lösa in.

### Svarsfält

Svaret är posten som lade tillbaka beloppet, i samma form som vid Lös in, med negativt `amountOre`.

### Statuskoder

- `200`: Beloppet är tillbakalagt.
- `404 redemption_not_found`: Posteringen finns inte på ditt företag.
- `409 already_reversed`: Den är ångråd tidigare. En gång, aldrig två.
- `409 not_reversible`: Posteringen är själv en ångran eller en kontant utbetalning.
- `409 card_inactive`: Kortet är annullerat eller fryst under en invänd.
- `409 order_refunded`: Beställningen bakom kortet är återbetald. Köparen har redan fått pengarna tillbaka, så en ångran skulle ge dem värdet två gånger.

### Anrop

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

### Exempel på svar · 200

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

## Skicka kortet igen

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

Skickar kortet till mottagaren igen. Det är vad en kassa gör när en kund säger att mejlet aldrig kom.

Utan `recipient` skickar vi det som kortets order ännu inte har fått levererat, till den adress som står angiven. Ett kort som redan har levererats skickas inte igen. Med `recipient` rättar du adressen på just det här kortet och skickar det dit, även om det har levererats tidigare. Bara aktiva kort skickas.

### Sökvägsparametrar

- `code` (`string`, obligatorisk): Kortets kod, t.ex. `KUV-7K9F-2XQD`.

### Body

- `recipient` (`string`, valfri): En ny e-postadress för kortet. Bodyn kan utelämnas helt.

### Svarsfält

Svaret anger hur mycket som skickades:

- `delivered` (`integer`): Antal kort som skickades. 0 betyder att det inte fanns något att skicka.

### Statuskoder

- `200`: Skickat, eller inget att skicka.
- `400 not_a_gift_card`: Koden i sökvägen är en biljettskod (TKT-…), inte ett presentkort.
- `404 gift_card_not_found`: Koden finns inte på ditt företag.
- `404 order_not_found`: Omskicka hittade kortet, men inte den order det kom från. Ett kort utfärdat för hand har ingen order och kan därför inte omskickas på det sättet.
- `409 card_has_no_delivery`: Kortet har aldrig skickats av oss, eftersom det lämnades ut över disken. Det finns ingen leverans att upprepa.
- `422 invalid_recipient`: `recipient` i body för omskicka är inte en e-postadress vi kan skicka till. Utelämna fältet för att skicka till den adress kortet redan har.
- `502 delivery_failed`: Vi nådde vår leverantör, och den avvisade försändelsen. Det är inget fel på kortet, och ingenting har dragits. Försök igen eller kontrollera adressen.

### Anrop

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

### Exempel på svar · 200

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

## Annullera ett kort

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

Annullerar kortet omedelbart. Saldot står kvar på kortet men kan inte längre användas, och det går inte att ångra. Är kortet redan annullerat får du tillbaka det som det är, utan fel.

### Sökvägsparametrar

- `code` (`string`, obligatorisk): Kortets kod, t.ex. `KUV-7K9F-2XQD`.

### Svarsfält

Kortet ligger under `giftCard`, nu med `status` `void`.

### Statuskoder

- `200`: Kortet är annullerat, eller var det redan.
- `400 not_a_gift_card`: Koden i sökvägen är en biljettskod (TKT-…), inte ett presentkort.
- `404 gift_card_not_found`: Koden finns inte på ditt företag.

### Anrop

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

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

## Återbetala en order

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

Skickar tillbaka hela betalningen för ordern till kortet som betalade, inklusive köparens expeditionsavgift, och Kuverts avgift på försäljningen går tillbaka till dig. Det är den enda slutpunkten som flyttar pengar.

När Stripe har bekräftat återbetalningen tas värdet av orderns kort, dess biljetter annulleras och order.refunded skickas. Det sker oftast några sekunder efter svaret.

Anropet tar ingen idempotensnyckel, eftersom orderns egen status är spärren. En order som redan har återbetalats svarar 409 order_not_refundable.

### Sökvägsparametrar

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

### Svarsfält

Svaret är återbetalningen så som Stripe tog emot den:

- `orderId` (`string`): Ordern.
- `refundId` (`string`): Stripes id för återbetalningen (`re_…`), som du hittar den under i Stripe.
- `amountOre` (`integer`): Beloppet som skickas tillbaka, i öre.

### Statuskoder

- `200`: Stripe har tagit emot återbetalningen.
- `404 order_not_found`: Ordern finns inte hos ditt företag.
- `409 order_not_refundable`: Ordern kan inte återbetalas: den är inte betald, redan återbetald eller fryst under en tvist.
- `409 merchant_not_payment_ready`: Ditt Stripe-konto är inte anslutet.
- `503 stripe_not_configured`: Betalningar är tillfälligt otillgängliga hos oss. Försök igen senare.

### Anrop

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

### Exempel på svar · 200

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