পেমেন্টের বার্তা
নিজের সার্ভারে স্বাক্ষরযুক্ত পেমেন্টের বার্তা নিন।
পেমেন্টের বার্তা চালু করুন
পেমেন্ট তৈরির সময় 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-signature | v1=hex | হ্যাঁ | HMAC-SHA256(timestamp + '.' + rawBody) |
x-ekmivo-timestamp | Unix seconds | হ্যাঁ | প্রতিবার পাঠানোর সময়ের নতুন টাইমস্ট্যাম্প। |
x-ekmivo-event-id | UUID | হ্যাঁ | একই ঘটনার স্থির পরিচয়; বার্তার 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));
পাঠানো ও পুনঃচেষ্টা
2xx হলে বার্তা পৌঁছেছে ধরা হয়। সংযোগের ত্রুটি, 408, 429 ও 5xx হলে মোট সর্বোচ্চ আটবার চেষ্টা হয়। বিরতি ১, ৫ ও ১৫ মিনিট, ১, ৩, ৬ ও ১২ ঘণ্টা; সঙ্গে ০–৩০ সেকেন্ডের ব্যবধান যোগ হয়। অন্য HTTP ত্রুটিতে পুনঃচেষ্টা বন্ধ হয়।
শুধু
status === "COMPLETED" হলে এবং invoice_id, পরিমাণ ও মুদ্রা অর্ডারের সঙ্গে মিললে সরবরাহ করুন। শুধু HTTP 200 মানে পেমেন্ট সম্পন্ন নয়।