Quickstart — Node.js
A working integration in about a hundred lines. Tested against Node 20 LTS with no extra dependencies beyond express and Node's built-ins.
1 · Project skeleton
mkdir lidya-quickstart && cd lidya-quickstart
npm init -y
npm install express
echo "node_modules" > .gitignoreAdd a .env with your sandbox keys (don't commit it):
LIDYA_API_KEY=sk_test_replace_me
LIDYA_WEBHOOK_SECRET=whsec_replace_me
LIDYA_BASE_URL=https://api.lidya.money
PORT=3000Load it before everything else — Node 20+ supports --env-file natively:
node --env-file=.env server.js2 · Create a charge
server.js:
import express from "express";
import { randomUUID, createHmac, timingSafeEqual } from "node:crypto";
const app = express();
// Use the raw body parser specifically on the webhook route so HMAC
// verification has the bytes Lidya signed. Everything else uses JSON.
app.use((req, res, next) =>
req.path === "/lidya-webhook" ? next() : express.json()(req, res, next),
);
app.post("/checkout", express.json(), async (req, res) => {
const charge = await fetch(`${process.env.LIDYA_BASE_URL}/v1/charges`, {
method: "POST",
headers: {
"X-API-Key": process.env.LIDYA_API_KEY,
"X-Idempotency-Key": randomUUID(),
"Content-Type": "application/json",
},
body: JSON.stringify({
amount: 12550, // ₺125.50 — minor units
currency: "TRY",
paymentMethod: "credit_card_international",
merchantReference: req.body.orderId,
customer: {
name: req.body.customerName,
email: req.body.customerEmail,
},
}),
}).then((r) => r.json());
if (charge.error) {
return res.status(502).json({ error: charge.error });
}
res.redirect(303, charge.paymentPageUrl);
});The X-Idempotency-Key makes the call safe to retry. Resending the same key for the same body returns the original response instead of creating a second charge.
3 · Verify the webhook
app.post("/lidya-webhook", express.raw({ type: "application/json" }), (req, res) => {
const signature = req.headers["lidya-signature"];
const timestamp = req.headers["lidya-timestamp"];
if (!signature || !timestamp) {
return res.status(401).send("missing headers");
}
// Reject anything older than 5 minutes — protects against replay
// attacks long after the original delivery.
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - Number(timestamp)) > 300) {
return res.status(401).send("stale signature");
}
const expected = createHmac("sha256", process.env.LIDYA_WEBHOOK_SECRET)
.update(`${timestamp}.`)
.update(req.body) // raw Buffer, not the parsed JSON
.digest("hex");
const a = Buffer.from(signature);
const b = Buffer.from(expected);
if (a.length !== b.length || !timingSafeEqual(a, b)) {
return res.status(401).send("invalid signature");
}
const event = JSON.parse(req.body.toString());
if (event.eventType === "charge.succeeded") {
// Mark the order paid. This handler must be idempotent — Lidya
// retries on 5xx and on missing 200, so the same event id can
// arrive more than once.
markOrderPaid(event.payment.merchantReference);
}
res.status(200).json({ received: true });
});
app.listen(process.env.PORT, () => {
console.log(`Lidya quickstart on :${process.env.PORT}`);
});The signing scheme is HMAC_SHA256(secret, "${timestamp}." + rawBody). The ${timestamp}. prefix prevents an attacker from replaying yesterday's body with a fresh timestamp header.
4 · Wire the webhook URL
Register the public URL of /lidya-webhook in the dashboard:
Dashboard → Webhooks → Add endpoint → paste your URL → tick charge.succeeded, charge.failed, payout.completed.
For local development use ngrok http 3000 and register the https://*.ngrok.app URL.
5 · Smoke test
curl -X POST http://localhost:3000/checkout \
-H "Content-Type: application/json" \
-d '{"orderId": "test-001", "customerName": "Test", "customerEmail": "test@example.com"}'Follow the Location redirect in a browser and complete the test card flow on checkout.lidya.money. Your webhook handler should log charge.succeeded within a few seconds.
What to read next
- Idempotency — the full key lifecycle, header rules, retry behaviour.
- Webhooks — event taxonomy, retry schedule, signature details.
- Errors — every code Lidya can return and how to react.
- Postman collection — every endpoint, ready to import.