Going Live
KYC onboarding, production keys, webhook hardening, and the pre-launch checklist.
Moving from sandbox to production changes three things: your integration now touches a real bank and real money, your account has to clear KYC before Kvell will process a live payment, and a handful of hardening steps that don't matter in simulation start to matter a lot. This guide is the checklist for that transition.
1. Complete KYC onboarding
Every merchant account is gated on KYC before it can process live payments. Business information β sole trader (TCKN) or company (VKN/MERSΔ°S) β is submitted through the merchant dashboard onboarding flow; from your integration, you only need to read the resulting status:
curl https://api.pay.kvell.group/v1/kyc/status \
-H "X-Api-Key: pk_live_a1b2c3d4e5f6g7h8" \
-H "X-Timestamp: 2026-04-13T10:00:00Z" \
-H "X-Signature: <signature>"{
"status": "approved",
"decidedAt": "2026-04-10T09:00:00Z",
"sessionId": "f1c9a2e6-b4d4-e8a9-c1f2-a7b8c9d0e1f3",
"subjectId": "sbj_3c4d5e6f7081"
}status is one of none, pending, approved, rejected, suspended, or expired. Payments are allowed only when it's approved β every other value gets a 403 KVELL-GW-4020 on any payment call. If you integrate on behalf of multiple sellers, each submerchant carries its own KYC decision too β see Marketplace & Split Payments.
2. Switch to production credentials
| Sandbox | Production | |
|---|---|---|
| Base URL | api.pay.sandbox.kvell.group | api.pay.kvell.group |
| API key prefix | pk_demo_β¦ | pk_live_β¦ |
| Secret key prefix | sk_demo_β¦ | sk_live_β¦ |
Generate a live key pair from the dashboard, point your base URL at production, and stop sending X-Sim-Scenario β it's silently ignored there, but a stray header is a good sign an environment got mixed up somewhere. Testing in the Sandbox has the full sandbox-vs-production comparison.
3. Harden your webhook endpoint
Sandbox testing tends to hide three things that matter immediately in production:
- Serve HTTPS. Kvell will not deliver to a plain-HTTP URL.
- Verify
X-Kvell-Signatureon every request before acting on the body β never trust an unsigned or mis-signed payload. - Dedupe on
X-Kvell-Event-Id, not just event type + payment id. A retried delivery gets a newX-Kvell-Deliveryvalue but the same event id. - Respond 2xx within 10 seconds. Acknowledge fast and do slow work (emails, ledger writes, fulfillment) asynchronously β a slow handler looks identical to a failing one and triggers a retry.
import crypto from 'node:crypto';
function verifyKvellSignature(rawBody, signatureHeader, secret) {
// signatureHeader looks like "sha256=<hex>"
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signatureHeader),
Buffer.from(expected)
);
}Register (or rotate) the endpoint from the merchant dashboard β endpoint management runs over your dashboard session, not the HMAC-signed public API. Webhooks has the full event list and retry schedule.
4. Go-live checklist
| Check | Why it matters |
|---|---|
Every write carries an idempotency fence (Idempotency-Key header, or transactionId for sessions/charges) | A retried request after a timeout must not double-charge or double-payout |
| Amounts are integer kuruΕ everywhere | 15000 means 150.00 TRY β a float anywhere in the pipeline is a bug |
You branch on 4xxx vs 5xxx error codes | 4xxx won't succeed on retry (fix the request); 5xxx might (retry with backoff) |
| Refund path is tested end-to-end | Including a partial refund and reading refundableAmount back |
| Reconciliation is wired up | GET /v1/transactions to list, POST /v1/transactions/exports for a scheduled CSV/PDF pull β match both against your own ledger |
| You know your rate limit | Default tier is 500 requests/sec sustained, 1,000 burst β design retries with backoff, not a tight loop |
Example: scheduling a reconciliation export
curl -X POST https://api.pay.kvell.group/v1/transactions/exports \
-H "X-Api-Key: pk_live_a1b2c3d4e5f6g7h8" \
-H "X-Timestamp: 2026-04-13T10:00:00Z" \
-H "X-Signature: <signature>" \
-H "Content-Type: application/json" \
-d '{
"format": "csv",
"filters": { "from": "2026-04-01T00:00:00Z", "to": "2026-04-30T23:59:59Z", "status": "captured" }
}'{
"exportId": "exp_3c4d5e6f7081",
"status": "queued"
}Poll until it's ready, then download it:
curl https://api.pay.kvell.group/v1/transactions/exports/exp_3c4d5e6f7081 \
-H "X-Api-Key: pk_live_a1b2c3d4e5f6g7h8" \
-H "X-Timestamp: 2026-04-13T10:00:05Z" \
-H "X-Signature: <signature>"{
"exportId": "exp_3c4d5e6f7081",
"status": "ready",
"downloadUrl": "/v1/transactions/exports/exp_3c4d5e6f7081/download",
"rowCount": 128
}Run this on a schedule β nightly or monthly β and diff rowCount and totals against your own order database. A persistent mismatch usually means a webhook was missed, not that Kvell's ledger is wrong.
5. Security notes
- Your secret key signs every request server-side β it never belongs in a browser, a mobile app, or a public repo.
- Rotate keys from the dashboard if one is ever exposed; the old key stops working immediately.
- Never log a raw card number. In practice you won't have one to log β hosted checkout and tokenization mean a PAN reaches Kvell's card vault and nowhere else in your stack.
Next steps
| Guide | What it covers |
|---|---|
| Testing in the Sandbox | Confirm every flow passes before you flip the switch |
| Marketplace & Split Payments | Selling on behalf of multiple sellers |
| API reference | Every endpoint, request, and response shape |