LidyaDocs

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" > .gitignore

Add 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=3000

Load it before everything else — Node 20+ supports --env-file natively:

node --env-file=.env server.js

2 · 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.