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.
- Your server creates a payment with
POST /api/v1/paymentsand gets acheckout_url. - 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.
- The gateway waits for the wallet's SMS on the owner's phone, and checks the TrxID, the amount and the sender's number.
- When it matches, the gateway calls your webhook with
payment.paidand sends the customer back to yoursuccess_url. - 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:
gp_live_…— the site key. Your server sends it asAuthorization: Bearer gp_live_…. Never put it in a web page or app bundle.whsec_…— the webhook secret, to check that a webhook really came from the gateway.
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" }
}'
| Field | Notes | |
|---|---|---|
amount | required | Taka, e.g. 490 or 490.50. |
reference | recommended | Your own order id. Asking again with the same reference and amount returns the same payment, not a new one — safe to retry. |
description | Shown on the checkout page. | |
customer | name, email, phone — for your records and the admin panel. | |
success_url, cancel_url | Where the customer goes afterwards. The gateway adds ?payment_id=pay_…. | |
metadata | Any JSON object (up to 4 KB). Comes back in the webhook. | |
expires_in_minutes | Overrides 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
| Status | Meaning |
|---|---|
pending | Link created, customer hasn't given a TrxID yet. |
submitted | TrxID given, waiting for the wallet's SMS. |
paid | Money confirmed. Deliver. |
review | Something didn't match (amount short, another sender, no SMS in time, above the auto-approve limit). The owner decides. |
rejected | The owner rejected it. Sent as a payment.rejected webhook. |
expired, cancelled | The 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.