Paperless API Reference
A unified payment gateway for Bangladesh — accept cards, bKash, Nagad, Rocket, and internet banking through a single, well-documented API built for developers.
All API requests must be made over HTTPS. The API accepts and returns JSON in all request and response bodies.
https://api.paperlessltd.com/v1Sandbox:
https://sandbox-api.paperlessltd.com/v1Quick Start
Get your first payment running in under 10 minutes. You’ll need a Paperless merchant account and your API keys from the dashboard.
Create a merchant account
Sign up at portal.paperlessltd.com. Complete KYC — approval usually within 24 hours.
Get your API keys
Find your merchant_id and api_key under Dashboard → Settings → API Keys.
Initiate a payment
Call POST /payments/initiate and redirect your customer to the returned payment_url.
Handle the webhook
Configure a webhook endpoint to receive real-time payment confirmations.
curl -X POST https://api.paperlessltd.com/v1/payments/initiate -H "Content-Type: application/json" -H "Authorization: Bearer YOUR_API_KEY" -d '{ "merchant_id": "PLMRCH_001", "amount": 5000, "currency": "BDT", "order_id": "ORD_20250101_001", "customer_name": "Rahim Uddin", "customer_email": "rahim@example.com", "redirect_url": "https://yoursite.com/success", "webhook_url": "https://yoursite.com/webhooks/paperless" }'
Authentication
Paperless uses Bearer token authentication. Pass your API key in every request via the Authorization header.
Authorization: Bearer pl_live_xxxxxxxxxxxxxxxxxxxxxxxxKey types
| Type | Prefix | Description |
|---|---|---|
| Live | pl_live_ | Real transactions on production. Never expose in client-side code. |
| Sandbox | pl_test_ | Test mode only. Safe for staging environments. |
Environments
Paperless maintains two fully isolated environments with separate credentials and data.
| Environment | API Base | Dashboard |
|---|---|---|
| Production | api.paperlessltd.com/v1 | portal.paperlessltd.com |
| Sandbox | sandbox-api.paperlessltd.com/v1 | sandbox-dashboard.paperlessltd.com |
SDKs & Libraries
Official libraries handle request signing, retries, and response parsing. All are open-source on GitHub.
Payments
The core of Paperless. Initiate a payment session and redirect your customer to the hosted checkout. The page handles provider selection, OTP, and confirmation entirely.
Returns a payment_url valid for 30 minutes. Redirect the customer there immediately after initiation.
Request body
| Field | Type | Description | |
|---|---|---|---|
| merchant_id | string | Required | Your merchant identifier from the dashboard. |
| amount | integer | Required | Amount in BDT paisa (1 BDT = 100 paisa). Min: 100. |
| currency | string | Required | Must be BDT. |
| order_id | string | Required | Your unique order reference. Used for idempotency. |
| customer_name | string | Required | Full name of the customer. |
| customer_email | string | Required | Customer email address. |
| customer_phone | string | Optional | E.164 format e.g. +8801700000000. |
| redirect_url | string | Required | Redirect here on success. |
| cancel_url | string | Optional | Redirect here if customer cancels. |
| webhook_url | string | Optional | Per-transaction webhook override. |
| metadata | object | Optional | Arbitrary key-value pairs echoed in webhook payloads. |
Response
{
"success": true,
"txn_id": "PL_TXN_9F3A2C8D",
"order_id": "ORD_20250101_001",
"status": "INITIATED",
"payment_url": "https://pay.paperlessltd.com/checkout/PL_TXN_9F3A2C8D",
"expires_at": "2025-01-01T11:30:00+06:00",
"amount": 5000,
"currency": "BDT"
}{
"success": false,
"error": {
"code": "INVALID_AMOUNT",
"message": "Amount must be a positive integer in paisa.",
"field": "amount"
}
}{
"success": false,
"error": {
"code": "UNAUTHORIZED",
"message": "Invalid or missing API key."
}
}Payment Status
Fetch the current state of any transaction by its txn_id. Useful for polling if you haven’t set up webhooks yet, or for verifying a webhook payload.
curl https://api.paperlessltd.com/v1/payments/PL_TXN_9F3A2C8D -H "Authorization: Bearer pl_live_xxxx"
{
"txn_id": "PL_TXN_9F3A2C8D",
"order_id": "ORD_20250101_001",
"status": "COMPLETED",
"amount": 5000,
"currency": "BDT",
"payment_method": "bkash",
"provider_ref": "BKH8736210000",
"completed_at": "2025-01-01T11:03:22+06:00"
}Possible statuses
Refunds
Issue full or partial refunds on any COMPLETED payment. Refunds are processed asynchronously — listen for the refund.completed webhook event to confirm.
| Field | Type | Description | |
|---|---|---|---|
| txn_id | string | Required | Transaction ID of the original completed payment. |
| amount | integer | Optional | Amount in paisa. Omit to refund the full amount. |
| reason | string | Optional | Reason for refund. Stored for audit. |
| idempotency_key | string | Optional | Unique key to prevent duplicate submissions. |
curl -X POST https://api.paperlessltd.com/v1/refunds -H "Authorization: Bearer pl_live_xxxx" -H "Idempotency-Key: refund-ORD_001" -d '{"txn_id":"PL_TXN_9F3A2C8D","amount":2500}'
Transactions
Retrieve a paginated list of all transactions for your merchant account. Useful for reconciliation and reporting.
| Query param | Type | Description |
|---|---|---|
| page | integer | Page number. Default: 1. |
| limit | integer | Results per page. Default: 20, max: 100. |
| status | string | Filter by status e.g. COMPLETED. |
| from | string | ISO 8601 start date. |
| to | string | ISO 8601 end date. |
Merchants
Merchant management endpoints require an admin API key (pl_admin_). Contact support@paperlessltd.com to request admin access.
403 Forbidden on these endpoints.Webhooks
Webhooks deliver real-time event notifications to your server. Configure your endpoint in Dashboard → Settings → Webhooks.
Payload structure
{
"event": "payment.completed",
"event_id": "evt_A1B2C3D4E5",
"timestamp": "2025-01-01T11:03:22+06:00",
"data": {
"txn_id": "PL_TXN_9F3A2C8D",
"order_id": "ORD_20250101_001",
"status": "COMPLETED",
"amount": 5000,
"payment_method": "bkash"
}
}Webhook Event Reference
| Event | Description |
|---|---|
| payment.initiated | A new payment session has been created. |
| payment.completed | Payment confirmed. Funds settle in the next batch. |
| payment.failed | Payment failed at the provider level. |
| payment.cancelled | Customer closed or cancelled the checkout. |
| payment.expired | Payment session timed out (30 minutes). |
| refund.initiated | Refund submitted to the provider. |
| refund.completed | Refund returned to the customer successfully. |
| refund.failed | Refund failed. Manual review may be required. |
| settlement.processed | Settlement batch sent to your bank account. |
Signature Verification
Every webhook includes a X-Paperless-Signature header — an HMAC-SHA256 of the raw body using your webhook secret. Always verify before processing.
const crypto = require('crypto'); function verifyWebhook(rawBody, signature, secret) { const expected = crypto .createHmac('sha256', secret) .update(rawBody, 'utf8') .digest('hex'); return crypto.timingSafeEqual( Buffer.from(expected), Buffer.from(signature) ); } app.post('/webhooks/paperless', (req, res) => { const sig = req.headers['x-paperless-signature']; if (!verifyWebhook(req.rawBody, sig, process.env.WH_SECRET)) return res.status(401).send('Invalid signature'); // handle event... res.sendStatus(200); });
function verifyWebhook($payload, $sig, $secret): bool { $expected = hash_hmac('sha256', $payload, $secret); return hash_equals($expected, $sig); } $payload = file_get_contents('php://input'); $sig = $_SERVER['HTTP_X_PAPERLESS_SIGNATURE']; if (!verifyWebhook($payload, $sig, $_ENV['WH_SECRET'])) { http_response_code(401); exit; }
Idempotency
Supply an Idempotency-Key header on any POST request to safely retry without the risk of duplicate operations.
Idempotency-Key: order-ORD_20250101_001-attempt-1order_id as the key. Keys expire after 24 hours. Duplicate requests within that window return the original cached response with a 200.Error Codes
All errors return a consistent JSON body. code is machine-readable; message is human-readable.
{
"success": false,
"error": {
"code": "INSUFFICIENT_BALANCE",
"message": "The customer wallet does not have sufficient balance.",
"field": null
}
}| HTTP | Meaning |
|---|---|
| 200 | Success. |
| 400 | Bad request — missing or invalid parameters. |
| 401 | Unauthorized — invalid or missing API key. |
| 403 | Forbidden — insufficient permissions. |
| 404 | Resource not found. |
| 409 | Conflict — duplicate order_id. |
| 422 | Unprocessable entity — validation failed. |
| 429 | Too many requests. See rate limits. |
| 500 | Server error. Contact support if persistent. |
Testing
Use the sandbox environment with these credentials. No real money moves and all transactions reset daily.
Test cards
| Card number | Expiry | CVV | Outcome |
|---|---|---|---|
| 4242 4242 4242 4242 | Any future | Any | Success |
| 4000 0000 0000 0002 | Any future | Any | Declined |
| 4000 0000 0000 9995 | Any future | Any | Insufficient funds |
Test mobile wallets
| Provider | Phone | OTP | Outcome |
|---|---|---|---|
| bKash | 01700000001 | 123456 | Success |
| Nagad | 01700000002 | 123456 | Success |
| Rocket | 01700000003 | 123456 | Success |
| bKash | 01700000099 | Any | Failed |
Rate Limits
Limits are per API key and reset every 60 seconds. When exceeded a 429 is returned with a Retry-After header.
| Endpoint | Requests / min |
|---|---|
| POST /payments/initiate | 120 |
| GET /payments/:txn_id | 300 |
| POST /refunds | 60 |
| GET /transactions | 60 |
| All others | 120 |
X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset.