
PayFly API documentation
Base URL: https://payfly.oisbd.site · All responses are JSON and include a request_id.
1. Authentication
Server-to-server calls use a merchant API key in the Authorization header. Create keys in Dashboard → API & Webhooks. Never put keys in browser code.
Authorization: Bearer pf_xxxxxxxxx.your_secret
2. Create order
POST /api/v1/orders - amount is in BDT (e.g. 500 or "500.00"); it is stored as poisha (50000). Sending the same merchant_reference again returns the existing order (idempotent).
curl -X POST https://payfly.oisbd.site/api/v1/orders \
-H "Authorization: Bearer $PAYFLY_KEY" -H "Content-Type: application/json" \
-d '{"merchant_reference":"ORDER-10001","amount":500,"currency":"BDT","customer_name":"Customer","customer_phone":"01XXXXXXXXX","payment_method":"bkash"}'
{ "success": true, "order_id": "PF-2026-000001", "payment_id": "PAY-9F82A1B3C4D5", "status": "PENDING",
"amount_minor": 50000, "checkout_url": "https://payfly.oisbd.site/pay/PAY-9F82A1B3C4D5", "expires_at": "2026-09-29T16:40:00Z", "request_id": "REQ-…" }3. Checkout
Redirect the customer to checkout_url. The page shows the receiving account and amount, and updates itself when the payment is verified. A customer-submitted Transaction ID is only a matching hint; it never marks an order paid.
4. Payment status
GET /api/v1/payments/{payment_id} GET /api/v1/orders/{order_id} GET /api/v1/merchant/transactions
Statuses: CREATED, PENDING, PROCESSING, MATCHED, PAID, FAILED, EXPIRED, CANCELLED, REFUNDED, MANUAL_REVIEW. Only trust PAID as "money received".
5. Payment events (Android device → PayFly)
POST /api/v1/payment-events is used by the PayFly Verify app. Requests are signed with the device's hardware-backed key (ECDSA P-256 / SHA-256):
signed string = METHOD \n PATH \n X-PayFly-Timestamp \n X-PayFly-Nonce \n hex(SHA-256(body)) headers: X-PayFly-Device, X-PayFly-Timestamp, X-PayFly-Nonce, X-PayFly-Content-SHA256, X-PayFly-Signature (base64 DER)
Amounts are integers in poisha. Idempotency key = SHA-256(provider | transaction_id | receiver | amount): re-sent events return the original result.
6. Webhooks
Configure an https URL in the dashboard. Events: payment.created, payment.processing, payment.matched, payment.paid, payment.failed, payment.expired, payment.refunded, payment.manual_review, payment.rejected.
{ "id": "evt_123", "type": "payment.paid", "created_at": "2026-09-29T16:30:00Z",
"data": { "payment_id": "PAY-…", "order_id": "PF-2026-000001", "amount": 50000, "currency": "BDT", "provider": "bkash", "transaction_id": "ABC123", "status": "PAID" } }
Headers: X-PayFly-Signature, X-PayFly-Timestamp, X-PayFly-Event, X-PayFly-Delivery-ID. Webhook consumers must be idempotent - deduplicate on X-PayFly-Delivery-ID or the event id.
Retries after failed attempts: 10s, 30s, 2m, 10m, 30m, then FAILED. Any 2xx response is success.
7. Verify webhook signatures
signature = "sha256=" + HMAC_SHA256(timestamp + "." + rawBody, webhook_secret). Use the raw request body, never re-encoded JSON. Reject timestamps older than 5 minutes.
PHP
$raw = file_get_contents('php://input');
$ts = $_SERVER['HTTP_X_PAYFLY_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_PAYFLY_SIGNATURE'] ?? '';
$ok = abs(time() - (int)$ts) <= 300 && hash_equals('sha256=' . hash_hmac('sha256', $ts . '.' . $raw, $secret), $sig);
if (!$ok) { http_response_code(401); exit; }
// mark order paid only if not already processed for this delivery id
http_response_code(200);
JavaScript (Node)
const crypto = require('crypto');
app.post('/payfly/webhook', express.raw({type: 'application/json'}), (req, res) => {
const ts = req.header('X-PayFly-Timestamp'), sig = req.header('X-PayFly-Signature');
const expected = 'sha256=' + crypto.createHmac('sha256', SECRET).update(ts + '.' + req.body).digest('hex');
const ok = Math.abs(Date.now()/1000 - Number(ts)) <= 300 && sig.length === expected.length && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
if (!ok) return res.sendStatus(401);
res.sendStatus(200);
});8. Errors
{ "success": false, "error": { "code": "PAYMENT_NOT_FOUND", "message": "Payment was not found." }, "request_id": "REQ-123456" }
Common codes: INVALID_API_KEY, VALIDATION_FAILED, REFERENCE_CONFLICT, NO_PAYMENT_ACCOUNT, PAYMENT_NOT_FOUND, RATE_LIMITED, INVALID_SIGNATURE, REPLAY_DETECTED, TIMESTAMP_OUT_OF_RANGE, DEVICE_REVOKED, INVALID_TRANSITION, INTERNAL_ERROR.
9. Idempotency
Orders: same merchant_reference → same order. Payment events: unique idempotency key + unique (provider, transaction_id, receiver). Webhooks: unique delivery_id.