Best Practices
Keys and tokens
What you store is not a token but your Ed25519 private key. Tokens last about five minutes; your application mints them at runtime.
- Keep the private key in a secrets manager or a file with restricted access
- Never log the
Authorizationheader - Rotate the key on a regular cadence; revoke immediately if you suspect compromise
- Do not revoke the old key before the new one is live — the moment a
kidis revoked, requests signed with it start returning401
The private signing key must never reach a browser, a mobile app, or a public repository. It stays server-side.
Cache the token
Minting a token per call adds needless load and latency.
// ✅ Right — hold it until it expires
let cached: { token: string; expiresAt: number } | null = null;
async function getToken() {
const now = Math.floor(Date.now() / 1000);
if (cached && cached.expiresAt - 30 > now) return cached.token;
const t = await mintToken();
cached = { token: t.access_token, expiresAt: t.expires_at };
return cached.token;
}
// ❌ Wrong — sign and mint on every request
async function handler() {
const { access_token } = await mintToken();
}The 30-second margin stops the token expiring while a call is in flight.
The token is opaque — there is nothing inside it to decode. Do not parse it or try to pull merchantId out of it.
Error handling
Branch on code, not on message. message is translatable and will change; code is part of the contract.
// ✅ Right
if (err.code === 'payment.card.declined') { … }
// ❌ Wrong — breaks silently when the translation changes
if (err.message.includes('declined')) { … }500·502·503·504→ retry with exponential backoff400·401·403·409·422→ do not retry; fix the request- Write the
requestIdfrom every error into your own logs — it is the one key a support request needs
Do not act before the decision
Get the fraud decision before you capture the money. Asking after the transaction has happened produces audit data, not protection.
Acting on force_3d is part of this: the point of selective friction is to let low-risk transactions through without 3DS and protect conversion.
Idempotency
When retrying a create-payment call, send the same Idempotency-Key; otherwise you open a second intent.
Idempotency-Key: intent_<your own transaction reference>Derive the key from your transaction rather than generating it randomly — on a retry you have to be able to produce the same value.
Consuming webhooks
In its first version, give your endpoint one job: verify the signature → write the event to your own queue → return 2xx. Do not run business logic synchronously; the delivery timeout defaults to 10 seconds.
- Always verify the signature. The
v2signature is an HMAC of the raw body — verify over the raw bytes without parsing. → Webhooks - Be idempotent on
x-lunixi-event-id. If you processed the event but your response was lost, the same event comes again. - During a long maintenance window, return
2xxquickly and queue the work rather than serving5xx— after 20 consecutive failures the endpoint is disabled automatically and does not recover on its own.
Amounts
Minor-unit integers in every product. 250000 = ₺2,500.00.
Send a decimal and the amount is off by a factor of 100 — silently, because the API received a valid number and has no reason to reject it.
When we are unreachable
Decide this in advance; an outage is not when you want to be making the call.
| Strategy | Risk | Use when |
|---|---|---|
| Fail open — proceed | Higher fraud exposure | Low-value, low-risk transactions |
| Fail closed — decline | More false positives | High-value or regulated transactions |
| Queue for review | Operational overhead | A balanced approach |
Our own posture is fail-closed. If a flow errors while running, the decision rises to at least review, and an existing block is never downgraded. So "I got no answer" and "I got allow" are not the same thing — keep them distinct on your side too.
Do not go live without publishing rules
An unconfigured merchant does not silently get allow: you get the policy default (typically review) with code fraud.decision.unconfigured. Publish your flow and watch it in shadow mode before going live. → Flows and Resolution
Related
- Authentication — signing and Step-Up
- Error Handling — the envelope and codes
- Webhooks — signature and retries