Developer documentation

Your website owns products and orders. PABEL PAY owns the payment. You send an opaque order_id, an amount and a currency; you receive payment status by signed webhook.

Base URL: https://pabelpay.pabelprimezone.shop/api/v1

Authentication

Every request is signed with your Secret Key. Send these headers:

X-Public-KeyYour public key (pk_…)
X-TimestampUnix seconds. Must be within ±5 minutes.
X-NonceUnique per request, 8–64 chars [A-Za-z0-9_-]. A reused nonce is rejected (replay protection).
X-Signaturehex HMAC-SHA256 of the canonical string below, keyed with the Secret Key
Idempotency-KeyOptional. Safe retries of payment/create.
canonical = METHOD + "\n" + PATH + "\n" + TIMESTAMP + "\n" + NONCE + "\n" + sha256_hex(RAW_BODY)
signature = hex( HMAC_SHA256(canonical, SECRET_KEY) )

PATH is the request path as sent (for example /api/v1/payment/create), without host or query string. For requests without a body, RAW_BODY is the empty string.

Create payment

POST /payment/create

{
  "order_id": "ORDER-20260928-10001",   // required, opaque, unique per website
  "amount": 500.00,                     // required, from YOUR database
  "currency": "BDT",                    // BDT, USD, EUR, GBP, INR
  "customer_name": "Rahim", "customer_email": "rahim@example.com", "customer_phone": "01700000000",
  "success_url": "https://yourshop.com/pay/return", "fail_url": "...", "cancel_url": "...",
  "webhook_url": "https://yourshop.com/pay/webhook"
}

Response 201:

{ "success": true, "payment_id": "PAY-…", "order_id": "ORDER-…", "amount": 500.0, "currency": "BDT",
  "checkout_url": "https://pabelpay.pabelprimezone.shop/checkout/PAY-%E2%80%A6", "status": "pending" }

Redirect the customer to checkout_url. Calling create again with the same order_id returns the existing pending payment; a different amount for the same order_id returns 409. Return URLs must be on your registered website's domain. Product fields (product_name, items, cart, sku, quantity, …) are rejected on purpose.

Other endpoints

GET /payment/{payment_id}Payment with status timeline
GET /payment/status/{payment_id}Current status
POST /payment/verify {"payment_id":"…"}Re-checks with the gateway (gateway payments). verified:true only when PAID.
POST /payment/cancel {"payment_id":"…"}Cancels a pending payment
GET /merchant/profileMerchant identity for your keys
GET /websiteWebsite settings for your keys
GET /pingConnectivity + signature check

Webhooks

Events: payment.created, payment.pending, payment.processing, payment.approved, payment.completed, payment.rejected, payment.failed, payment.cancelled. Payload contains payment data only:

{
  "event": "payment.completed",
  "event_id": "EVT-9F3A6B21C04D77E1",
  "payment_id": "PAY-1A2B3C4D5E6F7A8B",
  "order_id": "ORDER-20260928-10001",
  "merchant_id": "MER-XXXXXXXXXXXX",
  "website_id": "WEB-XXXXXXXXXXXX",
  "amount": 500.0,
  "currency": "BDT",
  "transaction_id": "TXN-XXXX",
  "status": "paid",
  "timestamp": "2026-09-28T10:15:00+00:00",
  "signature": "<64 hex chars, always the LAST member>"
}

Headers: X-PabelPay-Event, X-PabelPay-Event-Id, X-PabelPay-Timestamp, X-PabelPay-Signature. Respond with any 2xx within 15 seconds. Otherwise delivery is retried with backoff (1m, 5m, 15m, 1h, 3h, 12h, 24h) up to 8 attempts; you can also retry manually from the dashboard. The same event may arrive more than once — deduplicate by event_id.

Verifying webhooks

signature = hex( HMAC_SHA256( X-PabelPay-Timestamp + "." + UNSIGNED_BODY, WEBHOOK_SECRET ) )

UNSIGNED_BODY is the raw request body with the trailing ,"signature":"<hex>" member removed. Do not re-encode the JSON; strip that suffix from the raw string. Compare with a constant-time function and reject timestamps older than 5 minutes. Our PHP client does all of this in handleWebhook().

// Node.js
const m = raw.match(/^(.*),"signature":"([a-f0-9]{64})"\}$/s);
const unsigned = m[1] + '}';
const expected = crypto.createHmac('sha256', WEBHOOK_SECRET).update(ts + '.' + unsigned).digest('hex');
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(req.headers['x-pabelpay-signature']));

Statuses

Payment status (owned by PABEL PAY): pending, processing, approved, paid, failed, rejected, cancelled, expired. Order status (pending, confirmed, processing, completed, rejected, delivered…) belongs to your website and is never sent to or changed by us.

Errors & limits

Errors: {"success":false,"error":{"code":"…","message":"…"}}. Common codes: invalid_signature, timestamp_out_of_range, replay_detected, rate_limited (429), duplicate_order (409), product_data_not_allowed (422). Default limit: 120 requests/minute per website.

Rules to follow

  • Never mark an order paid because the customer reached your success URL — only a verified webhook (or verify) proves payment.
  • Take the amount from your own database, never from the browser.
  • Check that the webhook amount and currency match your order.
  • Approve, reject and fulfil orders in your admin panel.