Integration guide

How a shop or app takes payments through GalibPay. Your server talks to the gateway; the customer only ever sees the checkout page.

  1. Your server creates a payment with POST /api/v1/payments and gets a checkout_url.
  2. You send the customer to that link. They pick bKash / Nagad / Rocket / Upay (confirmed automatically) or Wise / bank transfer (checked by the owner), send the money and type the TrxID or reference.
  3. The gateway waits for the wallet's SMS on the owner's phone, and checks the TrxID, the amount and the sender's number.
  4. When it matches, the gateway calls your webhook with payment.paid and sends the customer back to your success_url.
  5. You deliver the product — from the webhook, never from the redirect alone.

1. Get a site key

In the GalibPay admin, open Sites → Add a site. You get two secrets, shown once:

2. Create a payment

curl https://galibpay.pages.dev/api/v1/payments \
  -H "Authorization: Bearer gp_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 490,
    "reference": "ORDER-1042",
    "description": "Pattern Pro — lifetime",
    "customer": { "name": "Rahim", "email": "rahim@example.com", "phone": "01712345678" },
    "success_url": "https://yourshop.com/thanks",
    "cancel_url": "https://yourshop.com/cart",
    "metadata": { "product": "pattern-pro-lifetime" }
  }'
FieldNotes
amountrequiredTaka, e.g. 490 or 490.50.
referencerecommendedYour own order id. Asking again with the same reference and amount returns the same payment, not a new one — safe to retry.
descriptionShown on the checkout page.
customername, email, phone — for your records and the admin panel.
success_url, cancel_urlWhere the customer goes afterwards. The gateway adds ?payment_id=pay_….
metadataAny JSON object (up to 4 KB). Comes back in the webhook.
expires_in_minutesOverrides the default link lifetime (5 minutes to 7 days).

Response (201):

{
  "id": "pay_k3m9x2p7q4w8zt",
  "status": "pending",
  "amount": 490,
  "reference": "ORDER-1042",
  "checkout_url": "https://galibpay.pages.dev/p/pay_k3m9x2p7q4w8zt",
  "expires_at": "2026-09-26T12:30:00.000Z",
  …
}

Redirect the customer to checkout_url.

3. Receive the webhook

When a payment is paid (or the owner rejects it), the gateway POSTs JSON to the site's webhook URL:

POST /your/webhook
X-GalibPay-Event: payment.paid
X-GalibPay-Signature: t=1790412345,v1=5f2c…

{ "event": "payment.paid",
  "data": { "id": "pay_…", "reference": "ORDER-1042", "status": "paid", "amount": 490,
            "paid_amount": 490, "method": "bkash", "trx_id": "BJQ7K2M9XA", "sender": "01712345678",
            "customer": { … }, "metadata": { … }, "paid_at": "2026-09-26T11:42:10.000Z" } }

Always check the signature, then deliver. Reply with any 2xx within 8 seconds. If your server doesn't answer, the gateway tries again after 1, 5 and 30 minutes, then 2 and 6 hours. Webhooks can arrive twice — deliver once per data.id.

Node.js / Cloudflare

async function verify(request, secret) {
  const body = await request.text();
  const sig = request.headers.get("X-GalibPay-Signature") || "";
  const t = sig.match(/t=(\d+)/)?.[1], v1 = sig.match(/v1=([a-f0-9]+)/)?.[1];
  if (!t || !v1 || Math.abs(Date.now() / 1000 - Number(t)) > 300) return null; // 5-minute window
  const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret),
    { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
  const mac = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(`${t}.${body}`));
  const hex = [...new Uint8Array(mac)].map((b) => b.toString(16).padStart(2, "0")).join("");
  return hex === v1 ? JSON.parse(body) : null;
}

// in your handler
const event = await verify(request, env.GALIBPAY_WEBHOOK_SECRET);
if (!event) return new Response("bad signature", { status: 400 });
if (event.event === "payment.paid") await deliverOrder(event.data.reference, event.data);
return new Response("ok");

PHP

$body = file_get_contents('php://input');
preg_match('/t=(\d+),v1=([a-f0-9]+)/', $_SERVER['HTTP_X_GALIBPAY_SIGNATURE'] ?? '', $m);
$ok = $m && abs(time() - (int)$m[1]) <= 300
   && hash_equals(hash_hmac('sha256', $m[1] . '.' . $body, getenv('GALIBPAY_WEBHOOK_SECRET')), $m[2]);
if (!$ok) { http_response_code(400); exit('bad signature'); }
$event = json_decode($body, true);
if ($event['event'] === 'payment.paid') deliver_order($event['data']['reference'], $event['data']);
echo 'ok';

4. Check a payment (optional)

On your success page, or any time, ask for the payment's current state:

curl https://galibpay.pages.dev/api/v1/payments/pay_k3m9x2p7q4w8zt \
  -H "Authorization: Bearer gp_live_xxx"

A customer can open your success_url by hand. Deliver on the webhook, or after checking status === "paid" here — never on the redirect alone.

Statuses

StatusMeaning
pendingLink created, customer hasn't given a TrxID yet.
submittedTrxID given, waiting for the wallet's SMS.
paidMoney confirmed. Deliver.
reviewSomething didn't match (amount short, another sender, no SMS in time, above the auto-approve limit). The owner decides.
rejectedThe owner rejected it. Sent as a payment.rejected webhook.
expired, cancelledThe link timed out, or the customer went back.

Errors

Errors are JSON — {"error": "…", "code": "…"} — with 400 for a bad request, 401 for a missing or wrong key, 403 for a switched-off site, 404 and 429 for too many requests.