Step 1
Sign a merchant token
The token is a JWT — a JSON payload with a signature stapled on, so the receiver can tell nobody edited it; its fields are called claims. Your backend mints one when a logged-in customer opens the payment flow, and it asserts exactly one thing: your own reference for that customer.
The claims we accept
| Claim | Value |
|---|---|
alg | HS256 — the only algorithm we accept. A token naming anything else is rejected. Strictly this lives in the token's header, not among the payload claims; the algorithm option in the snippet below is what sets it. |
iss | Your merchant id, exactly as Kvarn gave it to you (a UUID). |
aud | The literal string kvarn-pay. |
sub | Your own reference for the customer. It must be stable: the same customer must always get the same value, because it is what we key their record and their credited crypto to. |
iat | Required. It may not be dated in the future — your clock and ours are compared, with 30 seconds of tolerance. |
exp | Required, and no further out than iat plus 10 minutes. A longer expiry does not buy a longer token; it is rejected outright. |
merchantFeeBps | Optional, and only when Kvarn has enabled your own fee for your account. Your own fee on this payment, as a whole number of basis points — 1 bps is 0.01 %, so 150 is 1.5 %. Anything from 0 to 2000 (20 %). It is taken out of what the customer pays, on top of ours, shown to them as a row of its own labelled "<your business name> fee", and recorded as owed to you. Leave it out to charge nothing. Different pages of your app may sign different values — the fee belongs to the token, not to your account. See Fees. |
merchantFeeBps is the only claim of your own we read. Anything else you add is ignored — in particular we deliberately do not accept a name or an email from you. The verified name and email come from the identity check, never from an assertion, so nothing you send about the person can influence a KYC record.
A merchantFeeBps outside 0–2000, or one that is not a whole number, makes the whole token invalid: the session is refused with the same generic 401 as any other bad token. So does a fee above zero on an account where Kvarn has not enabled your own fee — your portal's overview says whether it is enabled; ask Kvarn to turn it on. The cap is a ceiling on what you may add, and it refuses figures that cannot be a rate at all — 15000 is never "1.5 %". It will not catch a unit slip inside the range: 1500 is a valid 15 % and your customer is charged 15 %. Check your units yourself — 150 is 1.5 %.
An endpoint your page can call
The natural shape is one authenticated endpoint on your own backend that mints a token for whoever is logged in. Using jsonwebtoken on Node:
js
import express from 'express';
import jwt from 'jsonwebtoken';
const app = express();
// Both handed to you by Kvarn. Read them from your secret store —
// YOUR_JWT_SECRET must never reach the browser.
const MERCHANT_ID = process.env.YOUR_MERCHANT_ID;
const JWT_SECRET = process.env.YOUR_JWT_SECRET;
app.post('/kvarnpay/token', requireLogin, (req, res) => {
const merchantToken = jwt.sign({}, JWT_SECRET, {
algorithm: 'HS256',
issuer: MERCHANT_ID,
audience: 'kvarn-pay',
subject: req.user.id, // your own reference for this customer
expiresIn: '10m',
});
res.json({ merchantToken });
});The payload holds only what you are telling us: issuer, audience, subject and expiresIn are options rather than claims, and they are what produce iss, aud, sub, iat and exp. So an integration that charges no fee of its own signs an empty payload. Guard the endpoint with your own session check — whoever can call it can open a KvarnPay session as the customer it names.
To charge your own fee, put it in that payload. The value may differ from call to call, so a checkout page that takes 1.5 % and a top-up page that takes nothing are two different tokens from the same endpoint:
js
const merchantToken = jwt.sign({ merchantFeeBps: 150 }, JWT_SECRET, {
algorithm: 'HS256',
issuer: MERCHANT_ID,
audience: 'kvarn-pay',
subject: req.user.id,
expiresIn: '10m',
});Because it rides on the signed token, your customer cannot edit it away and your page never has to be trusted with it.
Careful
Every rejection answers with one generic 401 invalid merchant token, on purpose. Naming the failing step would turn the endpoint into an oracle for guessing merchant ids, so the response body will never tell you which claim was wrong. Debug by re-checking your signing inputs — secret, merchant id, audience, clock — not by reading the error.