Issue a pass
Create a pass for one customer from a published card, and get back the links they tap to save it.
How issuing works
A card holds the design: colours, logo, strip picture and fields. Some fields are fixed, like your shop’s address. Others come from variables, like the customer’s name or how many stamps they have.
To issue a pass, you name a published card and give the values you want to set. Korti builds the pass and gives you three links: one that works on any phone, an Apple Wallet link and a Google Wallet link.
The pass keeps its own ID. You use it later to add stamps, change the balance or void the pass.
Before you start
You need two things from the dashboard.
- An API key. Go to Developers and choose Create key. Use a test key,
sk_test_…, while you build. - A published card. Drafts can’t issue passes.
What a test key can doA test key, sk_test_…, only ever issues trial passes. They work on both wallets, expire after seven days, are never billed and don’t count toward your trial cards. A test key can read, update and void only the passes test keys issued, and gets 404 for any other pass. A business can issue 100 test passes in any seven days; over that you get 403 test_limit. Passes issued with a live key behave as usual.
Issue the pass
These steps use the Kaffi Kría stamp card. It gives a free coffee after 10 stamps.
Find the card ID and its variables
List your cards with
GET /v1/cards. Each card says which values it takes, so you know what to send.customer.namestringShown on the front of the card. Up to 60 characters.values.stampsintegerStamps collected so far, from 0 to 10. Defaults to 0.
Create the pass
Send the card ID and the values. Keep your key on your server. Never put it in a web page or a mobile app.
curl https://korti.is/v1/passes \ -H "Authorization: Bearer $KORTI_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: member-58213" \ -d '{ "card": "3e0c5d1a-8f55-4a62-9a63-2f0b1f6c7d21", "customer": { "name": "Anna Guðrún", "email": "anna@example.is" }, "values": { "stamps": 3 } }'Give the customer the link
Send
urlhowever you already talk to customers, or set"sendEmail": trueand Korti emails it tocustomer.email. When they open it, Korti shows the right button for their phone: Add to Apple Wallet on an iPhone, Save to Google Wallet on Android.If you build your own page, link to
appleUrlandgoogleUrldirectly. More ways are in Hand out cards.
Custom values
A detail you add to a card in the designer, like Member number under Upplýsingar um korthafa, is a custom value. GET /v1/cards lists it in variables as custom.<key>, for example custom.member_number. You can send it in either form, and Korti stores the same thing:
{
"card": "3e0c5d1a-8f55-4a62-9a63-2f0b1f6c7d21",
"values": { "stamps": 3, "custom.member_number": "M-0042" }
}Send a key once. If the same key appears in both forms in one request, Korti refuses it with 422 invalid_request and names both. A custom key uses letters, numbers and underscores, up to 40 characters, and a value is text of up to 200 characters. A card can have 20 custom values.
memberNumber is a different valuememberNumber is Korti’s built-in membership number, for membership cards that show a number. It isn’t the same as a custom detail you named “Member number”: that one is custom.member_number. Set the one your design shows.
What you get back
A successful call returns 201 Created and the new pass. The links work straight away.
{
"id": "b7d3f0a2-1c4e-4f5a-8b6d-9e0f1a2b3c4d",
"object": "pass",
"card": "3e0c5d1a-8f55-4a62-9a63-2f0b1f6c7d21",
"serial": "KQ7M2X9F",
"status": "active",
"trial": false,
"expiresAt": null,
"customer": { "name": "Anna Guðrún", "email": "anna@example.is", "phone": "" },
"values": {
"stamps": 3, "points": 0, "balance": 0, "memberNumber": "",
"memberSince": "Oct 2026", "validUntil": "10 Oct 2027", "seat": "", "custom": {}
},
"rewardReady": false,
"warnings": [],
"saved": { "apple": false, "google": false },
"onDevice": { "apple": false, "google": false },
"url": "https://korti.is/p/Xk2v9QmT4rLp8sWz",
"appleUrl": "https://korti.is/p/Xk2v9QmT4rLp8sWz/apple",
"googleUrl": "https://korti.is/p/Xk2v9QmT4rLp8sWz/google",
"createdAt": "2026-10-10T10:24:31.000Z",
"updatedAt": "2026-10-10T10:24:31.000Z"
}Warnings
A request can succeed and still not give the pass everything you sent. The pass then has a warnings list, with a stable code and a message for people. It is empty when nothing is wrong.
valid_until_capped: a trial pass is never valid past its expiry, so avalues.validUntillater thanexpiresAtis replaced by that date. Every pass a test key issues is a trial pass, and so is a live-key pass before your business goes live. Themessagenames the date you sent and the date the pass shows;values.validUntilon the response is the date used. Passes issued after you go live keep the date you send.barcode_fallback: the card’s barcode value, filled in for this customer, can’t be encoded in the barcode format (for example an emoji in a Code 128 barcode) or looks like another card’s number, so the barcode holds the pass’s serial instead of no code. See Scanning.
valid_until_capped is set on the response to the request that sent the date (POST /v1/passes and PATCH /v1/passes/{id}); later reads show the date the pass has, with no warning. barcode_fallback describes the pass as it is now, so it stays on every read. A pass can have both at once.
Saved and on a phone
saved.apple turns true when the .pkpass file is downloaded. That is not the same as the card being on a phone: the customer can cancel the add, and your own scripts count as downloads. Use onDevice.apple to find out whether a card is on an iPhone: it is true once at least one iPhone has registered the pass with Apple Wallet to get updates, and false again if every device removes it. saved.google and onDevice.google are the same: the customer saved the pass to Google Wallet.
A validUntil sent as an ISO date, such as 2026-12-31, is written the way the card writes dates, in the card’s language (31 Dec 2026 or 31. des. 2026), like the date Korti fills in when you send none. Text in any other form is shown as you send it.
Retries and duplicates
Every successful call makes a new pass. If a request times out, you can’t tell whether Korti got it. Send an Idempotency-Key header with a value you choose, such as your own order or member ID. If you send the same key again within 24 hours, Korti returns the first response instead of making a second pass. Replays carry the header Idempotent-Replayed: true.
Heads upA customer who saves the same pass twice still has one card. A customer who gets two different passes has two cards, and their stamps split between them. Use an idempotency key so retries never do that.
To change a pass after it’s issued, see Send updates. Phones that saved it get the change within a few seconds.
API reference: issue a pass
Creates a pass from a published card and returns it with its save links.
Parameters
Send these as a JSON body.
cardstringRequiredID of a published card. Find it with GET /v1/cards.
customerobjectOptionalWho the pass is for.
namestringOptionalUp to 60 characters. Icelandic letters are fine.
emailstringOptionalNeeded if you set sendEmail.
phonestringOptionalUp to 30 characters. Stored for you; Korti doesn’t text it yet.
valuesobjectOptionalValues for the card’s variables. Send only the ones the card uses.
stampsintegerOptionalStamp cards. From 0 to the card’s total.
pointsintegerOptionalPoints cards. 0 or more.
balancenumberOptionalGift cards. Defaults to the card’s starting balance.
visitsLeftintegerOptionalKlippikort. Defaults to a full card. Use the punch and refill endpoints to change it. A refill is billed as one card, like issuing one, unless the pass is a trial or test pass.
memberNumber, memberSince, validUntil, seatstringOptionalKorti’s built-in text values, only if the design shows them. memberNumber is the membership number, not a holder detail you added yourself. validUntil as an ISO date (2026-12-31) is written in the card’s date format; a trial pass can’t be valid past its expiry, see Warnings. Moving validUntil later, or clearing it, on a pass that already had one is billed as one card, like issuing one, unless the pass is a trial or test pass; a first date and a shorter date are free.
customobjectOptionalYour own text values, used by fields like {{custom.favourite}}. Keys use letters, numbers and underscores. Each value is up to 200 characters.
custom.<key>stringOptionalThe same as custom: { <key>: … }, under the name GET /v1/cards lists. Send a key in one form only; both forms together get 422 invalid_request.
sendEmailbooleanOptionalEmail the customer a link to their card. Defaults to false.
Idempotency-KeyheaderOptionalAny unique string up to 255 characters. The same key within 24 hours returns the first pass.
Responses
- 201created
The pass exists and its links work.
- 400invalid_json
The body isn’t valid JSON.
- 401unauthorized
The API key is missing, wrong or revoked.
- 403trial_limit
All trial passes are used. Go live in Settings → Live cards: verify your company, add a payment card and set up Apple Wallet.
- 404not_found
No card with that ID in your business.
- 409card_not_published
The card is still a draft.
- 422invalid_value
A value breaks the card’s rules.
paramsays which one. - 429rate_limited
Too many requests. Wait the seconds in
Retry-After.
Errors
Every error has the same shape. type is stable, so branch on it. message is written for people and may change.
HTTP/1.1 409 Conflict
{
"error": {
"type": "card_not_published",
"message": "Kaffi Kría stamp card is a draft. Publish it in the dashboard, then try again.",
"param": "card"
}
}- What happened
- The card exists, but nobody has published it. Korti won’t issue passes from a draft, because customers would get a card you haven’t approved.
- What to do
- Open the card in the dashboard and choose Publish card. Then send the same request again. All error types are listed in Errors.