KortiDocs

Issue a pass

Create a pass for one customer from a published card, and get back the links they tap to save it.

Endpoint POST /v1/passesAbout 5 minutes

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.

  1. 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.
  2. 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 }
      }'
  3. Give the customer the link

    Send url however you already talk to customers, or set "sendEmail": true and Korti emails it to customer.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 appleUrl and googleUrl directly. 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.

Response201 Created
{
  "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 a values.validUntil later than expiresAt is 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. The message names the date you sent and the date the pass shows; values.validUntil on 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.

POST/v1/passeshttps://korti.is

Parameters

Send these as a JSON body.

cardstringRequired

ID of a published card. Find it with GET /v1/cards.

customerobjectOptional

Who the pass is for.

namestringOptional

Up to 60 characters. Icelandic letters are fine.

emailstringOptional

Needed if you set sendEmail.

phonestringOptional

Up to 30 characters. Stored for you; Korti doesn’t text it yet.

valuesobjectOptional

Values for the card’s variables. Send only the ones the card uses.

stampsintegerOptional

Stamp cards. From 0 to the card’s total.

pointsintegerOptional

Points cards. 0 or more.

balancenumberOptional

Gift cards. Defaults to the card’s starting balance.

visitsLeftintegerOptional

Klippikort. 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, seatstringOptional

Korti’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.

customobjectOptional

Your own text values, used by fields like {{custom.favourite}}. Keys use letters, numbers and underscores. Each value is up to 200 characters.

custom.<key>stringOptional

The 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.

sendEmailbooleanOptional

Email the customer a link to their card. Defaults to false.

Idempotency-KeyheaderOptional

Any 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. param says 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.