Skip to content

Gift cards

Eight endpoints cover the card's entire life through the API: issue, list, look up, redeem, reverse, resend, cancel and refund.

Cancel cannot be undone. There is no reactivation, because a card that was killed and later quietly revived is an obligation nobody has agreed to. Refund is the only endpoint that moves money: Stripe sends the amount back to the card that paid, and nowhere else.

Try it here on the page

The console below runs the same schemas and the same redemption rule as the API, against a card that exists only in your browser. Edit the body, and see what status your own call would get.

Console

Card
KUV-DLT2-9GPW
Balance
DKK 500.00
Status
active

Request

GET /v1/api/giftcards/KUV-DLT2-9GPW

Authorization: Bearer pk_test_…

No body. The code is in the path.

Response

200 OK

{
  "giftCard": {
    "id": "gc_3n8qk2vh",
    "code": "KUV-DLT2-9GPW",
    "orderId": null,
    "status": "active",
    "initialOre": 50000,
    "balanceOre": 50000,
    "expiresAt": "2029-01-15T12:00:00.000Z",
    "merchantSlug": "cafe-noir",
    "origin": "b2b",
    "reference": "Faktura 2026-114",
    "punchValueOre": null,
    "noCashClaim": false,
    "productName": null
  }
}

The balance is computed from the entries instead of being read from a field. That is why it always agrees with the redemptions.

Response 200 OK. The balance is computed from the entries instead of being read from a field. That is why it always agrees with the redemptions.

The console doesn't call out to the network. It runs the same schemas and the same redemption rule as the API, against a card that only exists in your browser. Status codes and error messages are therefore the ones your own integration gets.

Idempotency

Issue and redeem can both carry a key that makes the call safe to repeat. The same key twice answers with the result of the FIRST time (same card, same entry) instead of issuing or deducting twice. The two verbs do not detect the same thing, though: on REDEEM we answer 409 idempotency_key_conflict if the key has already been used on a different card or on a payout, instead of hitting the wrong one. On ISSUE we do not compare the call. If you reuse a key with a different amount, you get 201 and the FIRST card's data back, with no error. Use one key per card.

The key can be in three places. We read idempotencyKey in the body first, and otherwise the Idempotency-Key header and then X-Request-Guid. The body wins when there is both.

WhereName
BodyidempotencyKey: the explicit choice, and the one that wins.
HeaderIdempotency-Key: the name Stripe has made the standard.
HeaderX-Request-Guid, so an integration moving from Lifepeaks works without being rewritten.

Issue a card

POST/v1/api/giftcards

Issues a card outside the payment flow, for example sold from your own system, invoiced to a company or given on the house.

origin is required here, whereas it is optional in the dashboard. A system issues with no one watching, and the choice follows the card for the rest of its life: sold means a customer has paid for it, and then the card carries the customer's right to have the remaining balance paid out in cash. b2b and comp do not.

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"
  }'
201 CreatedJSON
{
  "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
  }
}
Try it in the console

Body

amountOreintegerRequired
The card's value in øre, at least 1 øre and at most DKK 100,000.00.
origin"sold" | "b2b" | "comp"Required
sold: a customer has paid. b2b: invoiced to a company. comp: given on the house.
validityMonthsintegerOptional
Validity in months, at least 36 and at most 600. If left out, the card is valid for 36.
referencestring | nullOptional
Your own note, e.g. an invoice number. At most 200 characters.
noCashClaimbooleanOptional
Mark the card «cannot be exchanged for cash», for example when it is for one particular thing. That does not by itself make an employee gift tax-free: SKAT, the Danish tax authority, treats digital gift cards as taxable. Only on b2b and comp.
idempotencyKeystringOptional
The same key twice issues only one card. At most 120 characters. Can also be sent as a header (see Idempotency).

Response fields

The card is under giftCard, in the same shape as for Look up a card, where the fields are described.

Status codes

  • 201

    The card has been issued. With an idempotencyKey that has been used before, the response is the card the key issued the first time.

  • 400invalid_issue_request

    The body doesn't match what issue requires.

  • 404merchant_not_found

    The key is valid, but the business behind it no longer exists. This is rare and cannot be fixed by a client. Contact us.

  • 409cash_claim_cannot_be_denied

    noCashClaim on a sold card. The customer has paid, and a flag cannot take away the right to have the remaining balance paid out.

An error always has the shape { "error": "…" }. Any call can also answer 401, 403, 429 and 500. See Errors.

List cards

GET/v1/api/giftcards

All of the business's cards, newest first, with the balance worked out from the ledger at that moment. The list is not paginated.

curl https://api.kuvert.dk/v1/api/giftcards \
  -H "Authorization: Bearer $KUVERT_API_KEY"
200 OKJSON
{
  "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"
    }
  ]
}

Response fields

The cards are under giftCards, each in the same shape as for Look up a card.

Status codes

  • 200

    The list, possibly empty.

An error always has the shape { "error": "…" }. Any call can also answer 401, 403, 429 and 500. See Errors.

Often used together with

Look up a card

GET/v1/api/giftcards/{code}

A card by its code, with the balance as it stands right now.

curl https://api.kuvert.dk/v1/api/giftcards/KUV-7K9F-2XQD \
  -H "Authorization: Bearer $KUVERT_API_KEY"
200 OKJSON
{
  "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
  }
}
Try it in the console

Path parameters

codestringRequired
The card's code, e.g. KUV-7K9F-2XQD.

Response fields

The card is under giftCard:

idstring
The card's id (gc_…). The code is what you look it up by.
codestring
The code printed on the card.
orderIdstring | null
The order the card came from. null for a card issued by hand or through the API.
status"active" | "redeemed" | "expired" | "void" | "disputed"
The card's state. Only active can be redeemed.
initialOreinteger
The card's value in øre, before anything was redeemed.
balanceOreinteger
The balance in øre, worked out from the entries and not read from a field.
expiresAtstring
When the card expires, ISO 8601 in UTC.
merchantSlugstring
The shop's address on shop.kuvert.dk.
origin"paid" | "sold" | "b2b" | "corporate" | "comp"
Where the value came from. paid was bought in the shop and corporate bought as a corporate gift; sold, b2b and comp were issued by hand or through the API.
referencestring | null
Your own note from the issue. null on a bought card.
punchValueOreinteger | null
The value of one punch if the card is a punch card, otherwise null. A punch card is redeemed in whole punches.
noCashClaimboolean
The card cannot be exchanged for cash.
productNamestring | null
The product's name as it read when the card was issued. null on a card issued by hand.

Status codes

  • 200

    The card.

  • 400not_a_gift_card

    The code in the path is a ticket code (TKT-…), not a gift card.

  • 404gift_card_not_found

    The code is not found on your business.

An error always has the shape { "error": "…" }. Any call can also answer 401, 403, 429 and 500. See Errors.

Redeem

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

Debits an amount from the card's balance. The code is in the path, the amount in the body.

Save the id. It is the posting, and it is the only thing that can reverse this specific redemption.

If it does not go through, the response is one of these. They are not all the same for the till: some are something the guest needs to know, and some are something the business needs to be called about.

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"
  }'
200 OKJSON
{
  "id": "red_8f21c0b45d3e4a9b8c7d6e5f4a3b2c1d",
  "code": "KUV-7K9F-2XQD",
  "amountOre": 6400,
  "remainingBalanceOre": 43600,
  "status": "active"
}
Try it in the console

Path parameters

codestringRequired
The card's code, e.g. KUV-7K9F-2XQD.

Body

amountOreintegerRequired
The amount in øre, at least 1. A punch card only takes a whole number of punches.
locationstring | nullOptional
Where it happened, e.g. «Vesterbro». At most 120 characters.
idempotencyKeystringOptional
The same key twice deducts only once. At most 120 characters. Can also be sent as a header (see Idempotency).

Response fields

The response is the ledger entry:

idstring
The entry's id (red_…), which you reverse it with.
codestring
The card's code.
amountOreinteger
The amount drawn, in øre.
remainingBalanceOreinteger
The balance after the redemption.
status"active" | "redeemed" | "expired" | "void" | "disputed"
status is the card's state after the redemption, one of active, redeemed, expired, void, disputed. If the balance is used up, it changes to redeemed.

Status codes

  • 200

    The amount has been deducted. With an idempotencyKey used before on the same card, the response is the one from the first time, and nothing is deducted again.

  • 400invalid_redeem_request

    The body does not match what redemption requires, typically an amount that is not a whole number of øre, or that is zero.

  • 400not_a_gift_card

    The code in the path is a ticket code (TKT-…), not a gift card.

  • 404gift_card_not_found

    The code is not found on your business.

  • 409card_inactive

    The card is not active: used up, cancelled, set to expired or frozen during a dispute. It does not clear on its own, so the business must be contacted.

  • 409redeem_too_soon

    The business has set a redemption delay, and it has not expired yet. The card is not wrong; it just cannot be used yet. Try again later.

  • 409idempotency_key_conflict

    The key has already been used on a different card or on a payout of the same card. See Idempotency.

  • 410gift_card_expired

    The card still shows as active, but the validity date has passed. Unlike card_inactive, it is time and not a decision that has closed the card.

  • 422insufficient_balance

    The amount is larger than the balance. We never deduct a partial amount on our own. Look up the card, deduct the balance and take the rest another way.

  • 422invalid_amount

    The redemption rule's own answer to an invalid amount. The schema normally catches it already as 400 invalid_redeem_request, so a client should be able to read both.

  • 422punch_amount_not_whole

    The card is a punch card, and the amount is not a whole number of punches. There is nothing wrong with the number, but it is the wrong kind of card for it. Look up the card, read punchValueOre, and send that value or a multiple of it. A punch card left with less than one punch is money neither the till nor the customer can reach.

An error always has the shape { "error": "…" }. Any call can also answer 401, 403, 429 and 500. See Errors.

Reverse a redemption

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

Puts the amount back on the card. This happens as a new entry with a negative amount that points to the one it reverses. Nothing is deleted, and both lines stay in the card's history. If the card was used up completely, it becomes active again.

curl -X POST https://api.kuvert.dk/v1/api/redemptions/red_8f21c0b45d3e4a9b8c7d6e5f4a3b2c1d/reverse \
  -H "Authorization: Bearer $KUVERT_API_KEY"
200 OKJSON
{
  "id": "red_2c77af90e1d24b3c9a8f7e6d5c4b3a21",
  "code": "KUV-7K9F-2XQD",
  "amountOre": -6400,
  "remainingBalanceOre": 50000,
  "status": "active"
}

Path parameters

idstringRequired
The redemption's id (red_…), which you got back from Redeem.

Response fields

The response is the entry that put the amount back, in the same shape as for Redeem, with a negative amountOre.

Status codes

  • 200

    The amount is put back.

  • 404redemption_not_found

    The posting is not found on your business.

  • 409already_reversed

    It has been reversed before. Once, never twice.

  • 409not_reversible

    The posting is itself a reversal or a cash payout.

  • 409card_inactive

    The card is cancelled or frozen under a dispute.

  • 409order_refunded

    The order behind the card has been refunded. The buyer already got the money back, so a reversal would give them the value twice.

An error always has the shape { "error": "…" }. Any call can also answer 401, 403, 429 and 500. See Errors.

Often used together with

Send the card again

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

Sends the card to the recipient again. That is what a till does when a customer says the email never arrived.

Without recipient, we send whatever the card's order has not yet had delivered, to the address it has. A card that has already been delivered is not sent again. With recipient, you correct the address on this particular card and send it there, even if it has been delivered before. Only active cards are sent.

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"
  }'
200 OKJSON
{
  "delivered": 1
}

Path parameters

codestringRequired
The card's code, e.g. KUV-7K9F-2XQD.

Body

recipientstringOptional
A new email address for the card. The body can be left out entirely.

Response fields

The response says how much was sent:

deliveredinteger
The number of cards sent. 0 means there was nothing to send.

Status codes

  • 200

    Sent, or nothing to send.

  • 400not_a_gift_card

    The code in the path is a ticket code (TKT-…), not a gift card.

  • 404gift_card_not_found

    The code is not found on your business.

  • 404order_not_found

    Resend found the card, but not the order it came from. A card issued by hand has no order and therefore cannot be resent that way.

  • 409card_has_no_delivery

    The card was never sent by us, because it was handed over the counter. There is no delivery to repeat.

  • 422invalid_recipient

    recipient in the body of resend is not an email address we can send to. Omit the field to send to the address the card already has.

  • 502delivery_failed

    We reached our provider, and it rejected the delivery. Nothing is wrong with the card, and nothing has been charged. Try again, or check the address.

An error always has the shape { "error": "…" }. Any call can also answer 401, 403, 429 and 500. See Errors.

Often used together with

Cancel a card

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

Cancels the card immediately. The balance stays on the card, but it can no longer be used, and this cannot be undone. If the card is already cancelled, you get it back as it is, without an error.

curl -X POST https://api.kuvert.dk/v1/api/giftcards/KUV-7K9F-2XQD/cancel \
  -H "Authorization: Bearer $KUVERT_API_KEY"
200 OKJSON
{
  "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
  }
}

Path parameters

codestringRequired
The card's code, e.g. KUV-7K9F-2XQD.

Response fields

The card is under giftCard, now with status void.

Status codes

  • 200

    The card has been cancelled, or already was.

  • 400not_a_gift_card

    The code in the path is a ticket code (TKT-…), not a gift card.

  • 404gift_card_not_found

    The code is not found on your business.

An error always has the shape { "error": "…" }. Any call can also answer 401, 403, 429 and 500. See Errors.

Often used together with

Refund an order

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

Sends the whole payment for the order back to the card that paid, including the buyer's handling fee, and Kuvert's fee on the sale goes back to you. It is the only endpoint that moves money.

Once Stripe has confirmed the refund, the value is taken off the order's cards, its tickets are cancelled, and order.refunded is sent. That usually happens a few seconds after the response.

The call takes no idempotency key, because the order's own status is the safeguard. An order that has already been refunded answers 409 order_not_refundable.

curl -X POST https://api.kuvert.dk/v1/api/orders/ord_7d3f0b9c1e2a45f68b4c9d0e1f2a3b5c/refund \
  -H "Authorization: Bearer $KUVERT_API_KEY"
200 OKJSON
{
  "orderId": "ord_7d3f0b9c1e2a45f68b4c9d0e1f2a3b5c",
  "refundId": "re_3PqX9aLkT2mN8vRb1c4D7eFg",
  "amountOre": 50500
}

Path parameters

idstringRequired
The order's id (ord_…): orderId on the card or in order.paid.

Response fields

The response is the refund as Stripe accepted it:

orderIdstring
The order.
refundIdstring
Stripe's id for the refund (re_…), which you find it under in Stripe.
amountOreinteger
The amount sent back, in øre.

Status codes

  • 200

    Stripe has accepted the refund.

  • 404order_not_found

    The order does not exist on your business.

  • 409order_not_refundable

    The order cannot be refunded: it is not paid, already refunded, or frozen during a dispute.

  • 409merchant_not_payment_ready

    Your Stripe account is not connected.

  • 503stripe_not_configured

    Payments are temporarily unavailable on our side. Try again later.

An error always has the shape { "error": "…" }. Any call can also answer 401, 403, 429 and 500. See Errors.

Often used together with

Was this page helpful?