মূল বিষয়ে যান
ekmivo.ডেভেলপার
ড্যাশবোর্ড
এই পাতায়বার্তা চালু করুনঘটনাহেডারসমূহস্বাক্ষর যাচাইপাঠানো ও পুনঃচেষ্টা

পেমেন্টের বার্তা

নিজের সার্ভারে স্বাক্ষরযুক্ত পেমেন্টের বার্তা নিন।

পেমেন্টের বার্তা চালু করুন

পেমেন্ট তৈরির সময় webhook_url দিন। API চাবির পাতা থেকে স্বাক্ষরের গোপন চাবি নিন। আলাদা ঠিকানা নিবন্ধন প্রয়োজন নেই।

json
{
  "amount": 150,
  "redirect_url": "https://merchant.example/success",
  "webhook_url": "https://merchant.example/api/payment-alerts"
}

ঘটনা

payment.verified
{
  "id": "EVENT_UUID",
  "type": "payment.verified",
  "invoice_id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "COMPLETED"
}

হেডারসমূহ

ফিল্ডধরনআবশ্যকবিবরণ
x-ekmivo-signaturev1=hexহ্যাঁHMAC-SHA256(timestamp + '.' + rawBody)
x-ekmivo-timestampUnix secondsহ্যাঁপ্রতিবার পাঠানোর সময়ের নতুন টাইমস্ট্যাম্প।
x-ekmivo-event-idUUIDহ্যাঁএকই ঘটনার স্থির পরিচয়; বার্তার id-এর সঙ্গে মেলান।

স্বাক্ষর যাচাই

# Signature verification belongs on your server.
# Do not verify a callback with cURL.
# Select Node.js, Python or PHP for receiver code.
  • JSON পড়ার আগে মূল বাইটগুলো দিয়ে স্বাক্ষর যাচাই করুন।
  • পাঁচ মিনিটের বেশি ব্যবধানের টাইমস্ট্যাম্প প্রত্যাখ্যান করুন।
  • পুনরাবৃত্তি আটকাতে ঘটনার পরিচয় সংরক্ষণ করুন। স্থায়ীভাবে গ্রহণের পরে 2xx দিন, তারপর পেমেন্ট যাচাই করে সরবরাহ করুন।
সম্পূর্ণ ব্যবহারিক উদাহরণ
Node.js · PostgreSQL
// Node.js 22+; npm install postgres
// Set DATABASE_URL, EKMIVO_API_ORIGIN, EKMIVO_API_KEY,
// EKMIVO_WEBHOOK_SECRET (API Keys → automatic webhook signing).
// Your order-creation code stores the returned invoice_id against its order.
// PostgreSQL schema (run once in YOUR merchant database):
// CREATE TABLE orders(id text PRIMARY KEY, invoice_id uuid UNIQUE NOT NULL,
//   amount_minor bigint NOT NULL, currency text NOT NULL DEFAULT 'BDT',
//   status text NOT NULL DEFAULT 'pending');
// CREATE TABLE ekmivo_events(id uuid PRIMARY KEY, received_at timestamptz DEFAULT now());
// CREATE TABLE fulfilment_outbox(order_id text PRIMARY KEY REFERENCES orders(id),
//   created_at timestamptz DEFAULT now());
import {createServer} from "node:http";
import {createHmac, timingSafeEqual} from "node:crypto";
import postgres from "postgres";
const sql = postgres(process.env.DATABASE_URL, {max: 4});
const api = new URL(process.env.EKMIVO_API_ORIGIN);
if (api.protocol !== "https:") throw new Error("HTTPS API origin required");
const apiKey = process.env.EKMIVO_API_KEY;
const secret = process.env.EKMIVO_WEBHOOK_SECRET;
if (!apiKey || !secret) throw new Error("Server credentials missing");

async function confirmOrder(invoiceId, eventId) {
  const response = await fetch(new URL("/v1/payment/verify", api), {
    method: "POST", headers: {Authorization: "Bearer " + apiKey,
      "Content-Type": "application/json"},
    body: JSON.stringify({invoice_id: invoiceId}),
    signal: AbortSignal.timeout(15000)
  });
  if (!response.ok) throw new Error("Verify API unavailable");
  const payment = await response.json();
  if (payment.invoice_id !== invoiceId || payment.status !== "COMPLETED")
    throw new Error("Payment is not completed; retry later");
  await sql.begin(async tx => {
    const [order] = await tx`SELECT * FROM orders
      WHERE invoice_id=${invoiceId}::uuid FOR UPDATE`;
    if (!order || String(order.amount_minor) !== String(payment.amount_minor) ||
        order.currency !== payment.currency) throw new Error("Order mismatch");
    if (eventId) await tx`INSERT INTO ekmivo_events(id) VALUES(${eventId}::uuid)
      ON CONFLICT DO NOTHING`;
    if (order.status === "paid") return;
    if (order.status !== "pending") throw new Error("Order needs review");
    await tx`UPDATE orders SET status='paid' WHERE id=${order.id}`;
    // An idempotent worker fulfils/enrols from this durable outbox. Do not
    // perform an external side effect inside a retryable DB transaction.
    await tx`INSERT INTO fulfilment_outbox(order_id) VALUES(${order.id})
      ON CONFLICT DO NOTHING`;
  });
}
// For a redirect-only integration, call confirmOrder(storedInvoiceId) from
// your authenticated return handler. Ignore browser-supplied payment status.

createServer(async (req, res) => {
  if (req.method !== "POST" || req.url !== "/api/ekmivo/webhook") {
    res.writeHead(404).end(); return;
  }
  try {
    const chunks = []; let size = 0;
    for await (const chunk of req) {
      size += chunk.length;
      if (size > 65536) {res.writeHead(413).end(); return;}
      chunks.push(chunk);
    }
    const raw = Buffer.concat(chunks);
    const timestamp = String(req.headers["x-ekmivo-timestamp"] || "");
    const signature = String(req.headers["x-ekmivo-signature"] || "");
    const expected = createHmac("sha256", secret)
      .update(timestamp + ".").update(raw).digest();
    const fresh = /^[0-9]+$/.test(timestamp) &&
      Math.abs(Date.now()/1000 - Number(timestamp)) <= 300;
    if (!fresh || !/^v1=[a-f0-9]{64}$/.test(signature) ||
        !timingSafeEqual(expected, Buffer.from(signature.slice(3), "hex"))) {
      res.writeHead(401).end(); return;
    }
    const event = JSON.parse(raw.toString("utf8"));
    if (event.id !== req.headers["x-ekmivo-event-id"] ||
        !/^[a-f0-9-]{36}$/i.test(event.id || "")) {
      res.writeHead(400).end(); return;
    }
    if (event.type !== "payment.verified") {res.writeHead(200).end(); return;}
    const invoiceId = event.invoice_id || event.data?.payment_intent_id;
    if (!/^[a-f0-9-]{36}$/i.test(invoiceId || "")) {
      res.writeHead(400).end(); return;
    }
    await confirmOrder(invoiceId, event.id);
    res.writeHead(200).end("ok");
  } catch {
    // Non-2xx keeps delivery retryable. Never log request bodies or credentials.
    res.writeHead(503).end("retry later");
  }
}).listen(Number(process.env.PORT || 3000));
GitHub-এ দেখুন ↗

পাঠানো ও পুনঃচেষ্টা

2xx হলে বার্তা পৌঁছেছে ধরা হয়। সংযোগের ত্রুটি, 408, 429 ও 5xx হলে মোট সর্বোচ্চ আটবার চেষ্টা হয়। বিরতি ১, ৫ ও ১৫ মিনিট, ১, ৩, ৬ ও ১২ ঘণ্টা; সঙ্গে ০–৩০ সেকেন্ডের ব্যবধান যোগ হয়। অন্য HTTP ত্রুটিতে পুনঃচেষ্টা বন্ধ হয়।

শুধু status === "COMPLETED" হলে এবং invoice_id, পরিমাণ ও মুদ্রা অর্ডারের সঙ্গে মিললে সরবরাহ করুন। শুধু HTTP 200 মানে পেমেন্ট সম্পন্ন নয়।