Reference
Environments
| Environment | Widget origin | API origin |
|---|---|---|
| Pilot / staging | https://staging.pay.kvarnx.com | https://api.staging.pay.kvarnx.com |
| Production | To be announced. | To be announced. |
Staging runs against sandbox card processing and sandbox identity checks: no real money and no real documents. Your staging merchant id and secret are not your production ones, and neither is a webhook endpoint registered for staging nor an API key created there.
Token checklist
- alg
HS256, always. Nothing else is accepted.- iss
- Your merchant id (a UUID), exactly as issued.
- aud
- The literal string
kvarn-pay. - sub
- Your reference for the customer. Stable, one per customer.
- iat
- Present, and not in the future. 30 seconds of clock tolerance.
- exp
- Present, and at most
iat+ 10 minutes. - merchantFeeBps
- Optional, and only when Kvarn has enabled your own fee for your account. A whole number of basis points,
0–2000. Omit it to charge no fee of your own. - everything else
- Ignored. Do not send a name or an email — we will not use them.
Troubleshooting
| Symptom | Likely cause |
|---|---|
| The widget opened fine, then a later attempt is rejected | The token is older than 10 minutes. Sign a fresh one each time the customer opens the flow. |
| Every token is rejected, and the code looks right | Your server's clock is off by more than 30 seconds. Check that it is synchronised. |
| A new integration is rejected from the first call | aud is not exactly kvarn-pay, or iss is not your merchant id — a display name or a staging id used against production will both fail. |
| Rejected only on the pages where you charge a fee | merchantFeeBps is not a whole number between 0 and 2000 — 1.5 or the string "150" are both refused — or your own fee is not enabled for your account (the portal's overview says; ask Kvarn). The 401 looks like any other bad token. |
| Rejected after rotating credentials | The secret and the merchant id no longer match each other. They are a pair; a token is verified with the secret belonging to the id in iss. |
| The widget says it was opened without a session | No token reached it — usually a token put in the query string instead of after the #, or an iframe whose src was set before the token arrived. A session that has failed or expired cannot be restarted from inside the iframe either: fetch a fresh token and set src again. |
| The identity step cannot open the camera | The iframe's allow attribute is missing or incomplete. It must carry camera; payment. |
| No event signature ever verifies | The body was parsed and re-serialized before hashing. Hash the raw request bytes — or the secret belongs to the other environment. |
| The same event is delivered more than once | Expected, not a fault. Delivery is at-least-once; dedupe on the event id. |