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.
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.
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.
| Where | Name |
|---|---|
| Body | idempotencyKey: the explicit choice, and the one that wins. |
| Header | Idempotency-Key: the name Stripe has made the standard. |
| Header | X-Request-Guid, so an integration moving from Lifepeaks works without being rewritten. |
Issue a card
/ v1/ api/ giftcardsIssues 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
}
}Body
amountOreintegerRequired- The card's value in øre, at least 1 øre and at most DKK 100,000.00.
origin"sold" | "b2b" | "comp"Requiredsold: 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
b2bandcomp. 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
idempotencyKeythat has been used before, the response is the card the key issued the first time.400
invalid_issue_requestThe body doesn't match what issue requires.
404
merchant_not_foundThe key is valid, but the business behind it no longer exists. This is rare and cannot be fixed by a client. Contact us.
409
cash_claim_cannot_be_deniednoCashClaimon asoldcard. 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.
Often used together with
List cards
/ v1/ api/ giftcardsAll 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
/ 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
}
}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.
nullfor a card issued by hand or through the API. status"active" | "redeemed" | "expired" | "void" | "disputed"- The card's state. Only
activecan 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.
paidwas bought in the shop andcorporatebought as a corporate gift;sold,b2bandcompwere issued by hand or through the API. referencestring | null- Your own note from the issue.
nullon 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.
nullon a card issued by hand.
Status codes
200
The card.
400
not_a_gift_cardThe code in the path is a ticket code (TKT-…), not a gift card.
404
gift_card_not_foundThe 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
Redeem
/ v1/ api/ giftcards/ {code}/ redeemDebits 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"
}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"statusis the card's state after the redemption, one ofactive,redeemed,expired,void,disputed. If the balance is used up, it changes toredeemed.
Status codes
200
The amount has been deducted. With an
idempotencyKeyused before on the same card, the response is the one from the first time, and nothing is deducted again.400
invalid_redeem_requestThe body does not match what redemption requires, typically an amount that is not a whole number of øre, or that is zero.
400
not_a_gift_cardThe code in the path is a ticket code (TKT-…), not a gift card.
404
gift_card_not_foundThe code is not found on your business.
409
card_inactiveThe 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.
409
redeem_too_soonThe 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.
409
idempotency_key_conflictThe key has already been used on a different card or on a payout of the same card. See Idempotency.
410
gift_card_expiredThe 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.422
insufficient_balanceThe 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.
422
invalid_amountThe 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.422
punch_amount_not_wholeThe 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.
Often used together with
Reverse a redemption
/ v1/ api/ redemptions/ {id}/ reversePuts 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.
404
redemption_not_foundThe posting is not found on your business.
409
already_reversedIt has been reversed before. Once, never twice.
409
not_reversibleThe posting is itself a reversal or a cash payout.
409
card_inactiveThe card is cancelled or frozen under a dispute.
409
order_refundedThe 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
/ v1/ api/ giftcards/ {code}/ resendSends 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.
400
not_a_gift_cardThe code in the path is a ticket code (TKT-…), not a gift card.
404
gift_card_not_foundThe code is not found on your business.
404
order_not_foundResend 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.
409
card_has_no_deliveryThe card was never sent by us, because it was handed over the counter. There is no delivery to repeat.
422
invalid_recipientrecipientin 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.502
delivery_failedWe 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
/ v1/ api/ giftcards/ {code}/ cancelCancels 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.
400
not_a_gift_cardThe code in the path is a ticket code (TKT-…), not a gift card.
404
gift_card_not_foundThe 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
/ v1/ api/ orders/ {id}/ refundSends 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_…):orderIdon the card or inorder.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.
404
order_not_foundThe order does not exist on your business.
409
order_not_refundableThe order cannot be refunded: it is not paid, already refunded, or frozen during a dispute.
409
merchant_not_payment_readyYour Stripe account is not connected.
503
stripe_not_configuredPayments 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?