# API

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

The Partner API lets your own system issue, look up, redeem and reverse gift cards with an API key. Your website can also fetch events, series, prices and table packages without a key (see Events below), and webhooks send notice about gift cards, orders, tickets, table bookings and events. It is ordinary HTTP with JSON, and there is no SDK to install.

## Base URL

```
https://api.kuvert.dk
```

## Authentication

All calls are authenticated with an API key in the `Authorization` header as a bearer token.

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

Keys are created under Settings → API keys. Only the owner can create and revoke them. There are two prefixes, and you don't choose between them:

| Prefix | Used for |
| --- | --- |
| pk_test_ | Testing. Does not touch real money. |
| pk_live_ | Live. Works on real cards and balances. |

Your shop sells in exactly one Stripe environment, and the key gets that environment. A demo shop gets a pk_test_ key; a shop selling for real money gets a pk_live_ key. Keys from the other environment are rejected on the first call. That is why there is no button that can pick the wrong one.

> **Important:** The key is only shown once, when it is created. We only store a hash of it. If you lose it, revoke it and create a new one. Never put it in code that is sent to a browser.

All calls are tied to the key's business. You cannot see or touch another business's cards, and a key only works while the business is active. A business that has not yet completed Stripe, or that is suspended, gets 401.

## Response format

Responses are JSON. A single gift card lives under the key giftCard, a list under giftCards.

Amounts are always integers in øre, and times are ISO 8601 in UTC. Every response carries an x-request-id header. Include it when you contact us about a specific call, and we can find exactly that one.

Lists are not paginated: GET /v1/api/giftcards answers with all of the business's cards at once, newest first.

## Errors

An error always has the same shape: an object with one field called error.

```json
{ "error": "unauthorized" }
```

| Status | error | Means |
| --- | --- | --- |
| 400 | invalid_issue_request | The body doesn't match what issue requires. |
| 400 | invalid_redeem_request | The body doesn't match what redeem requires. |
| 400 | bad_request | The call is missing something fundamental, typically the code in the path. |
| 400 | not_a_gift_card | The code in the path is a ticket code (TKT-…). It belongs to check-in, not to a gift card. The response is 400 and not 404, so you do not go looking for a card that never existed. |
| 401 | unauthorized | The key is missing, has been revoked, or the business is not active. |
| 404 | gift_card_not_found | The code doesn't exist on this business. |
| 409 | idempotency_key_conflict | The key was used on a DIFFERENT card. A reused key only repeats exactly the call it was used for, and it can never succeed on a new card. |
| 409 | cash_claim_cannot_be_denied | noCashClaim cannot be set on a SOLD card. A card the customer has paid for carries the right to have the remaining balance paid out in cash, and a flag cannot remove that right. Issue as `b2b` or `comp`. |
| 404 | merchant_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. |
| 404 | order_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. |
| 409 | card_has_no_delivery | The card was never sent by us, because it was handed over the counter. There is no delivery to repeat. |
| 422 | invalid_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. |
| 502 | delivery_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. |
| 403 | key_environment_mismatch | The key is valid, but from the other Stripe environment than the one your shop sells in, for example a pk_test_ on a shop that sells for real money, or the other way round. 403 and not 401: signing in again will not help, you need the other key. |
| 404 | not_found | The path does not exist. Check the method and the path. A POST to a path that only takes GET also gets this response. |
| 429 | rate_limited | Too many calls too close together. The Retry-After header says how many seconds until you may call again. See Limits. |
| 500 | internal_error | An unexpected error on our side. Nothing in the response says which; send us the x-request-id header and we will find it. |

Redemption also has its own responses about the card's state, the validity, the delay and the balance. They are listed together in the table under Redeem on the Gift cards page. One of them recurs: Reverse a redemption also answers with card_inactive, but on a narrower condition. There it only means a cancelled or frozen card, and it has its own table further down.

> **Note:** The error field is always called error and never message. Write your error handling accordingly.

## Limits

You can make 240 calls per 60 seconds to /v1/api, counted in fixed windows. If you go over, we answer 429 rate_limited with a Retry-After header saying how many seconds are left of the window.

> **Note:** The limit counts per IP address, not per key. If several tills sit behind the same public IP, they share the 240 calls.

## All endpoints

- `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`
- `GET /v1/api/webhooks`
- `POST /v1/api/webhooks`
- `PATCH /v1/api/webhooks`
- `DELETE /v1/api/webhooks/{id}`
