Developer Docsv1
● REST APIServer-to-Serverv1 · Integration Guide▶ Playground

Payment Gateway Integration Guide

Everything a developer needs to accept payments with Paperless — from creating a merchant account to receiving a verified payment notification. A hosted-checkout gateway for Bangladesh, built around a single, well-documented REST API.

📅 Updated July 2026|📖 20 min read|👤 For merchants & developers

Paperless is a hosted payment gateway. You call one server-to-server endpoint to create a payment session, redirect your customer to a Paperless-hosted checkout page, and receive the result back via redirect and a server-to-server IPN. Paperless handles provider selection, the payment UI, and confirmation — you never touch card or wallet credentials.

Who should use this

Merchants and developers integrating online payments into a website, mobile app, or backend. Because the API is server-to-server and IP-restricted, you integrate it from your backend, not the browser.

Supported payment methods

Payments are collected on the hosted checkout page. bKash (Tokenized Checkout) is the live provider; additional mobile-wallet and card providers are surfaced on the same checkout page as they come online, with no change to your integration.

bKashMore providers on the hosted checkout

High-level flow

Create a session → redirect the customer → they pay on the hosted page → Paperless redirects them back and fires an IPN → you verify and fulfil. The full picture is in Payment Flow.

Call the API from your server onlyThe initiate endpoint requires your secret credentials and a whitelisted IP. Never expose X-App-Secret in frontend code, mobile apps, or browser requests.

Base URL

Paperless uses a single base URL. There is no separate sandbox host — you switch between test and live behaviour with the mode field in the request body (SANDBOX or LIVE). Your API credentials are issued per mode.

EnvironmentBase URLSelected by
Productionhttps://api.paperlessltd.com"mode": "LIVE"
Sandboxhttps://api.paperlessltd.com"mode": "SANDBOX"

All endpoints are versioned under the /api/v1 prefix, for example:

Base URL
https://api.paperlessltd.com/api/v1/payments/initiate
JSON over HTTPSAll requests and responses are JSON over HTTPS. Send Content-Type: application/json on every request that has a body.

Merchant Onboarding

Before you can call the API you need an approved merchant account and a set of API credentials.

1

Create a merchant account

Sign up on the merchant portal with your business details.

2

Complete KYC

Submit the required business and identity documents for verification.

3

Wait for approval

Our team reviews your submission. You’ll be notified once the account is approved.

4

Log in to the merchant panel

Access your dashboard to manage payments, refunds, and settings.

5

Generate API credentials

Create your App Key and App Secret for each mode (sandbox / live).

6

Configure the IP whitelist

Add the public IP(s) of the server that will call the API. Requests from other IPs are rejected.

Whitelist first, integrate secondAPI requests only work from whitelisted IP addresses. If your allowlist is empty, even a valid credential pair returns 403 FORBIDDEN.

API Credentials

Server-to-server calls are authenticated with an App Key and an App Secret, generated in the merchant panel and issued separately for each mode. Dashboard read endpoints use a JWT from the auth service instead.

Public-ish

X-App-Key

Identifies your merchant account on server-to-server calls. Sent on every initiate request.

Secret

X-App-Secret

Proves the request is really from you. Treat it like a password — server-side only.

Reference

merchant_id

A UUID identifying your account. Returned on payment objects — you don’t send it; it’s resolved from your credentials.

Find and rotate these under Merchant Panel → Settings → API Credentials. Each mode has its own key pair, so a SANDBOX key never touches real money.

Never expose the App SecretThe X-App-Secret must never appear in frontend applications, mobile app bundles, browser network calls, or version control. If it leaks, rotate it immediately from the panel.

IP Whitelist

Every server-to-server request is checked against your merchant IP allowlist. This is a hard security boundary: even with a correct App Key and Secret, a request from an unlisted IP is rejected before it reaches the payment logic.

Why it’s required

It ensures that a leaked credential pair cannot be used from an attacker’s machine. Only traffic originating from your known server IPs is trusted.

How to add one

In Merchant Panel → Settings, add the public IPv4 address of the server that will call the API. Add every server that makes calls (app servers, workers, staging). Paperless honours the real client IP from X-Real-IP / X-Forwarded-For when you sit behind a reverse proxy.

What happens if the IP isn’t whitelisted

ConditionTypeDescription
No IPs configured403no whitelisted IPs configured — please add your server IP in the merchant panel
IP not in the list403request origin IP is not whitelisted for this merchant
Find your server IPFrom your server run curl https://api.ipify.org to see the exact public IP Paperless will observe. Add that value to the allowlist.

Authentication

Paperless uses different auth for different audiences:

Used forMechanismEndpoints
Server-to-serverApp Key + Secret headers + mode in body + IP whitelistPOST /payments/initiate
Merchant dashboardJWT Bearer from the auth serviceGET /payments, /refunds, logs, timeline
Status lookupPublic — anyone with the payment UUIDGET /payments/{id}

Server-to-server headers

Pass both credential headers on every initiate call. There is no request-body signature — the App Secret header is the proof of identity.

HTTP Headers
X-App-Key:    app_key_live_9f3a2c8d4b7e1a06
X-App-Secret: app_secret_live_c1d4e7f0a3b6c9d2
Content-Type: application/json

Dashboard (JWT) header

Read endpoints expect a bearer token obtained by logging in to the auth service:

HTTP Header
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
No HMAC request signingUnlike some gateways, Paperless does not require you to compute a request signature. Keep the App Secret private and restrict your IPs — those two controls are the security model.

Payment Flow

The end-to-end lifecycle of a single payment, from your server to a verified result:

Your merchant site
customer clicks “Pay”
POST /payments/initiate
server-to-server
Redirect customer
→ payment_url
Customer pays
hosted checkout · bKash
Gateway processes
provider confirms
Redirect back
success / fail / cancel
IPN sent
POST → your ipn_url
You verify & fulfil
GET /payments/{id}
The golden ruleRedirect and IPN both tell you a payment finished — but you only mark an order paid after verifying with GET /payments/{id}.

Initiate Payment

Creates a payment session and returns a payment_url. Redirect your customer there to complete the payment. Call this from your server only.

POST/api/v1/payments/initiate App Key + SecretCreate a payment session

Headers

HeaderTypeDescription
X-App-KeystringRequiredYour merchant App Key.
X-App-SecretstringRequiredYour merchant App Secret — keep private.
Content-TypestringRequiredMust be application/json.

Request body

FieldTypeDescription
modestringRequiredSANDBOX for testing, LIVE for real transactions. Also validated during authentication.
order_idstringRequiredYour order reference. Multiple payment attempts may share the same value.
total_amountstringRequiredAmount to charge as a positive decimal string, e.g. "1500.00". Not an integer.
currencystringRequiredISO 4217 code. Only BDT is supported currently.
success_urlstringRequiredRedirect URL after a successful payment.
fail_urlstringRequiredRedirect URL after a failed payment.
cancel_urlstringRequiredRedirect URL when the customer cancels.
customer_namestringOptionalCustomer’s full name.
customer_emailstringOptionalCustomer’s email address.
customer_phonestringOptionalCustomer’s phone number, e.g. 01711000000.
ipn_urlstringOptionalServer-to-server POST notification URL, called on every terminal state. See IPN.

Validation rules

  • mode is required and must be exactly SANDBOX or LIVE.
  • total_amount must be a positive decimal string, e.g. "1500.00".
  • currency must be BDT.
  • success_url, fail_url, and cancel_url are all required and must be valid URLs.

Responses

201Payment session created
{
  "payment_id": "550e8400-e29b-41d4-a716-446655440000",
  "token": "chk_7f3a2c8d4b7e1a06c1d4e7f0",
  "payment_url": "https://pay.paperlessltd.com/checkout/chk_7f3a2c8d4b7e1a06c1d4e7f0",
  "status": "pending",
  "expires_at": "2026-06-17T10:05:00Z"
}
400Validation error
{
  "code": "BAD_REQUEST",
  "message": "mode is required in request body (SANDBOX or LIVE)",
  "request_id": "req_a1b2c3d4e5f6"
}
401Missing / invalid credentials
{
  "code": "UNAUTHORIZED",
  "message": "X-App-Key and X-App-Secret headers are required",
  "request_id": "req_a1b2c3d4e5f6"
}
403IP not whitelisted
{
  "code": "FORBIDDEN",
  "message": "request origin IP is not whitelisted for this merchant",
  "request_id": "req_a1b2c3d4e5f6"
}

Code samples

cURL
curl -X POST https://api.paperlessltd.com/api/v1/payments/initiate \
  -H "X-App-Key: app_key_live_9f3a2c8d4b7e1a06" \
  -H "X-App-Secret: app_secret_live_c1d4e7f0a3b6c9d2" \
  -H "Content-Type: application/json" \
  -d '{
    "mode": "LIVE",
    "order_id": "ORD-2026-001",
    "total_amount": "1500.00",
    "currency": "BDT",
    "success_url": "https://yoursite.com/payment/success",
    "fail_url": "https://yoursite.com/payment/fail",
    "cancel_url": "https://yoursite.com/payment/cancel",
    "customer_name": "Rahim Uddin",
    "customer_email": "rahim@example.com",
    "customer_phone": "01711000000",
    "ipn_url": "https://yoursite.com/payment/ipn"
  }'
JavaScript (fetch)
// Run this on your SERVER — never in the browser.
const res = await fetch("https://api.paperlessltd.com/api/v1/payments/initiate", {
  method: "POST",
  headers: {
    "X-App-Key": process.env.PAPERLESS_APP_KEY,
    "X-App-Secret": process.env.PAPERLESS_APP_SECRET,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    mode: "LIVE",
    order_id: "ORD-2026-001",
    total_amount: "1500.00",
    currency: "BDT",
    success_url: "https://yoursite.com/payment/success",
    fail_url: "https://yoursite.com/payment/fail",
    cancel_url: "https://yoursite.com/payment/cancel",
    ipn_url: "https://yoursite.com/payment/ipn",
  }),
});

const data = await res.json();
// Redirect the customer to the hosted checkout:
res.status === 201 && redirect(data.payment_url);
Axios
const axios = require("axios");

const { data } = await axios.post(
  "https://api.paperlessltd.com/api/v1/payments/initiate",
  {
    mode: "LIVE",
    order_id: "ORD-2026-001",
    total_amount: "1500.00",
    currency: "BDT",
    success_url: "https://yoursite.com/payment/success",
    fail_url: "https://yoursite.com/payment/fail",
    cancel_url: "https://yoursite.com/payment/cancel",
    ipn_url: "https://yoursite.com/payment/ipn",
  },
  {
    headers: {
      "X-App-Key": process.env.PAPERLESS_APP_KEY,
      "X-App-Secret": process.env.PAPERLESS_APP_SECRET,
    },
  }
);

// data.payment_url → send the customer here
PHP (cURL)
$ch = curl_init("https://api.paperlessltd.com/api/v1/payments/initiate");
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_POST           => true,
  CURLOPT_HTTPHEADER     => [
    "X-App-Key: " . getenv("PAPERLESS_APP_KEY"),
    "X-App-Secret: " . getenv("PAPERLESS_APP_SECRET"),
    "Content-Type: application/json",
  ],
  CURLOPT_POSTFIELDS => json_encode([
    "mode"         => "LIVE",
    "order_id"     => "ORD-2026-001",
    "total_amount" => "1500.00",
    "currency"     => "BDT",
    "success_url"  => "https://yoursite.com/payment/success",
    "fail_url"     => "https://yoursite.com/payment/fail",
    "cancel_url"   => "https://yoursite.com/payment/cancel",
    "ipn_url"      => "https://yoursite.com/payment/ipn",
  ]),
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
// header("Location: " . $data["payment_url"]);
Python (requests)
import os, requests

resp = requests.post(
    "https://api.paperlessltd.com/api/v1/payments/initiate",
    headers={
        "X-App-Key": os.environ["PAPERLESS_APP_KEY"],
        "X-App-Secret": os.environ["PAPERLESS_APP_SECRET"],
    },
    json={
        "mode": "LIVE",
        "order_id": "ORD-2026-001",
        "total_amount": "1500.00",
        "currency": "BDT",
        "success_url": "https://yoursite.com/payment/success",
        "fail_url": "https://yoursite.com/payment/fail",
        "cancel_url": "https://yoursite.com/payment/cancel",
        "ipn_url": "https://yoursite.com/payment/ipn",
    },
    timeout=15,
)
data = resp.json()
# redirect(data["payment_url"])
After you get 201Immediately redirect the customer to payment_url. The session (and its token) expires at expires_at; if the customer waits too long they must restart.

Redirect Flow

When the payment reaches a terminal state, Paperless redirects the customer’s browser back to one of the three URLs you supplied, with result parameters appended to the query string.

URLCalled when
success_urlThe payment completed successfully.
fail_urlThe payment failed at the provider level.
cancel_urlThe customer cancelled or abandoned the checkout.

Returned query parameters

ParamTypeDescription
payment_idstringThe Paperless payment UUID. Present on all statuses.
transaction_idstringThe Paperless transaction reference. Present on all statuses.
order_idstringYour original order reference. Present on all statuses.
statusstringOne of success, failed, cancelled. Present on all statuses.
amountstringAmount charged. Present on success only.
currencystringCurrency code. Present on success only.

Example success redirect

Redirect URL
https://yoursite.com/payment/success
  ?payment_id=550e8400-e29b-41d4-a716-446655440000
  &transaction_id=TXN-20260617-A4F2
  &order_id=ORD-2026-001
  &status=success
  &amount=1500.00
  &currency=BDT
Redirects are not proof of paymentA user can bookmark, replay, or tamper with the redirect URL, and the browser may never reach your site at all. Always confirm with the IPN and/or Verify APIbefore fulfilling the order.

IPN — Instant Payment Notification

If you supply an ipn_url when initiating, Paperless sends a server-to-server POST to that URL the moment the payment reaches a terminal state — even if the customer closed their browser before the redirect. This is the most reliable signal that a payment finished.

Why trust IPN over the redirect

The redirect depends on the customer’s browser completing the round-trip. The IPN is sent directly from Paperless to your server, so it arrives regardless of what the customer’s device does.

Request

A POST with Content-Type: application/json and the payload below. paid_at is present only when status is success.

POSTIPN payload → your ipn_url
{
  "payment_id": "550e8400-e29b-41d4-a716-446655440000",
  "transaction_id": "TXN-20260617-A4F2",
  "order_id": "ORD-2026-001",
  "status": "success",
  "amount": "1500.00",
  "currency": "BDT",
  "merchant_receivable": "1455.00",
  "paid_at": "2026-06-17T10:05:00Z",
  "timestamp": "2026-06-17T10:05:01Z"
}

Verification & retry behaviour

The IPN is not cryptographically signed today, and it is delivered as a single attempt with a 5-second timeout (there is no automatic redelivery). Because of both facts, the IPN is a trigger, not proof: when you receive one, call GET /api/v1/payments/{payment_id} and trust that response.

No signature, single deliveryDo not fulfil orders from the raw IPN body. Verify by re-fetching the payment, and don’t rely on the IPN alone — also reconcile with the Verify API as a fallback in case a delivery is missed.
Recommended Best PracticeYour IPN endpoint should return an HTTP 200 OK status code. This confirms successful receipt of the notification and prevents our system from retrying the request.

Recommended implementation

Node.js (Express)
// POST /payment/ipn — called server-to-server by Paperless.
app.post("/payment/ipn", express.json(), async (req, res) => {
  const { payment_id, status } = req.body;

  // 1) Respond fast so Paperless doesn't time out (5s limit).
  res.sendStatus(200);

  // 2) Idempotency — skip if we already finalized this payment.
  if (await alreadyProcessed(payment_id)) return;

  // 3) NEVER trust the IPN body alone — re-fetch the source of truth.
  const verify = await fetch(
    `https://api.paperlessltd.com/api/v1/payments/${payment_id}`
  ).then((r) => r.json());

  if (verify.status === "success") {
    await fulfilOrder(verify.order_id, verify.total_amount);
  }
});
PHP
// POST /payment/ipn.php
$body = json_decode(file_get_contents("php://input"), true);
http_response_code(200); // ack immediately

$paymentId = $body["payment_id"];
if (already_processed($paymentId)) return;

// Re-fetch the authoritative status before fulfilling.
$verify = json_decode(file_get_contents(
  "https://api.paperlessltd.com/api/v1/payments/" . $paymentId
), true);

if ($verify["status"] === "success") {
  fulfil_order($verify["order_id"], $verify["total_amount"]);
}

Verify Payment

The source of truth for any payment is its detail endpoint. It is public — anyone with the payment UUID can read the latest status, so it’s safe to call from your backend (or even a status page) without credentials.

GET/api/v1/payments/{id} PublicGet full details / status

Returns status, the fee breakdown, provider details, customer info, and timestamps.

cURL
curl https://api.paperlessltd.com/api/v1/payments/550e8400-e29b-41d4-a716-446655440000
200Full payment details
{
  "payment_id": "550e8400-e29b-41d4-a716-446655440000",
  "order_id": "ORD-2026-001",
  "transaction_id": "TXN-20260617-A4F2",
  "status": "success",
  "mode": "LIVE",
  "currency": "BDT",
  "total_amount": "1500.00",
  "actual_paid_amount": "1500.00",
  "gateway_fee": "30.00",
  "vat_amount": "15.00",
  "total_fee": "45.00",
  "merchant_receivable": "1455.00",
  "provider": "bkash",
  "provider_trx_id": "BKH8736210000",
  "customer_name": "Rahim Uddin",
  "customer_email": "rahim@example.com",
  "customer_phone": "01711000000",
  "paid_at": "2026-06-17T10:05:00Z",
  "created_at": "2026-06-17T10:00:00Z",
  "updated_at": "2026-06-17T10:05:00Z"
}

Payment statuses

pendingprocessingsuccessfailedcancelledexpiredrefunded
Fulfil on success onlyOnly treat an order as paid when status is success. Cross-check total_amount and currency against your order before releasing goods.

Refund Flow

Refunds follow a review workflow. A merchant requests the refund from the panel; an admin issues it. Issuing is admin-only — merchant credentials can read refunds but cannot create them.

Customer requests a refund
Merchant creates a refund request
from the merchant panel
Admin reviews
Admin issues or rejects it
POST /admin/payments/{id}/refund
Merchant tracks the status
GET /refunds

Refund statuses

pendingsucceededfailed

A refund starts pending while the provider processes it, then settles to succeeded or failed (with a failure_reason).

Issue a refund Admin

POST/api/v1/admin/payments/{id}/refund JWT BearerAdmin only

{id} is the payment UUID. Full or partial amounts are supported.

FieldTypeDescription
amountstringRequiredAmount to refund as a positive decimal string. Must not exceed the paid amount.
reasonstringRequiredReason for the refund — stored for audit and shown in the panel.
StatusMeaning
200Refund accepted / succeeded.
400Invalid amount or the payment is not refundable.
409A refund is already in progress for this payment.
422The provider does not support refunds.
502The provider failed to process the refund.

List refunds JWT

GET/api/v1/refunds JWT BearerMerchant sees own, admin sees all

Paginated, newest first. Filter with ?payment_id= or ?status=.

cURL
curl https://api.paperlessltd.com/api/v1/refunds \
  -H "Authorization: Bearer <JWT>"
200Paginated refunds
{
  "data": [
    {
      "id": "a1b2c3d4-0000-0000-0000-000000000001",
      "payment_id": "550e8400-e29b-41d4-a716-446655440000",
      "amount": "500.0000",
      "reason": "customer request",
      "status": "succeeded",
      "provider_refund_trx_id": "RFND12345",
      "created_at": "2026-06-30T10:15:00Z"
    }
  ],
  "total": 1,
  "page": 1,
  "limit": 20,
  "total_pages": 1
}

To read one refund by its refund id, call GET /api/v1/refunds/{id} (same object shape).


Error Handling

Every error returns a consistent JSON body. code is a stable machine-readable string, message is human-readable, and request_id identifies the request in our logs — include it when contacting support.

{
  "code": "FORBIDDEN",
  "message": "no whitelisted IPs configured — please add your server IP in the merchant panel",
  "request_id": "req_a1b2c3d4e5f6"
}
HTTPCodeMeaning
400BAD_REQUESTMissing or malformed fields — e.g. no mode, invalid amount, bad JSON.
401UNAUTHORIZEDMissing/invalid X-App-Key + X-App-Secret, or an invalid/expired JWT on dashboard routes.
403FORBIDDENCredentials are valid but the request-origin IP is not whitelisted (or no IPs are configured).
404NOT_FOUNDThe payment, refund, or route does not exist.
409CONFLICTThe resource is in a conflicting state — e.g. a refund is already in progress, or the payment was already processed.
422UNPROCESSABLEThe provider does not support this operation (e.g. a refund it cannot process).
429RATE_LIMITEDToo many requests from your IP. Slow down and retry with backoff.
500INTERNAL_SERVER_ERRORUnexpected server error. Retry later; contact support with the request_id if it persists.
502PROVIDER_ERRORThe upstream provider (e.g. bKash) returned an error. Safe to retry.
503SERVICE_UNAVAILABLEA dependency (merchant service) is temporarily unavailable. Retry shortly.

Webhook / IPN Best Practices

1

Re-verify before acting

Since the IPN is unsigned, always call GET /payments/{id} and trust that response, not the POST body.

2

Prevent duplicate processing

Key your fulfilment on payment_id (or order_id) and make it idempotent — a repeated notification must not double-ship or double-credit.

3

Store transaction IDs

Persist payment_id and transaction_id so you can reconcile against your orders and our dashboard.

4

Respond quickly

Acknowledge with 200 within the 5-second window, then do the heavy work asynchronously.

5

Handle missed deliveries

There is no automatic retry, so run a periodic reconciliation job that verifies any payment still marked pending on your side.


Security Best Practices

  • Never expose the App Secret. Keep it in server-side environment variables, never in client code or git.
  • Always use HTTPS for your success / fail / cancel / IPN URLs.
  • Verify every payment with GET /payments/{id} before fulfilling — never trust the redirect or raw IPN.
  • Keep your IP allowlist tight. List only the servers that actually call the API, and remove stale entries.
  • Make all API calls server-side. The browser must never see your credentials.
  • Rotate credentials periodically, and immediately if you suspect a leak.
  • Validate amounts and currency from the verified payment against your own order before releasing goods.

Testing

Test end-to-end without moving real money by sending "mode": "SANDBOX" with your sandbox App Key / Secret. Everything else — the base URL, endpoints, and payload shape — is identical to live.

1

Generate sandbox credentials

Create a sandbox App Key / Secret pair in the merchant panel.

2

Whitelist your test server IP

The IP check applies in sandbox too — add your dev/staging server IP.

3

Initiate with mode: SANDBOX

Redirect to the returned payment_url and complete the flow on the sandbox checkout.

4

Exercise every outcome

Drive a payment to success, failure, and cancel, and confirm your redirect + IPN handlers behave for each.

Sandbox provider credentialsbKash sandbox runs against its tokenized-checkout test environment. Use the sandbox wallet credentials from your merchant panel to complete a test payment — they’re issued with your sandbox onboarding rather than hard-coded here.

SDK Examples

There is no proprietary SDK to install — the API is plain HTTPS + JSON, so any HTTP client works. The code samples above cover the core initiate call; the cards below point at the idiomatic client for each stack.

Laravel (Http facade)

Laravel / PHP
$res = Http::withHeaders([
    'X-App-Key'    => config('services.paperless.key'),
    'X-App-Secret' => config('services.paperless.secret'),
])->post('https://api.paperlessltd.com/api/v1/payments/initiate', [
    'mode'         => 'LIVE',
    'order_id'     => $order->id,
    'total_amount' => number_format($order->total, 2, '.', ''),
    'currency'     => 'BDT',
    'success_url'  => route('pay.success'),
    'fail_url'     => route('pay.fail'),
    'cancel_url'   => route('pay.cancel'),
    'ipn_url'      => route('pay.ipn'),
]);

return redirect($res->json('payment_url'));

FAQ

Why am I getting 401 Unauthorized?

Either the X-App-Key / X-App-Secret headers are missing, or the pair is invalid for the mode you sent. Both headers are required on every /payments/initiate call, and the credentials are mode-specific — a LIVE key will not authenticate a SANDBOX request.

On dashboard routes (/payments, /refunds), a 401 means the Authorization: Bearer JWT is missing or expired.

Why is my IP blocked (403 Forbidden)?

Server-to-server calls only work from a whitelisted IP. A 403 means your request’s origin IP is not in the merchant’s allowlist — or no IPs are configured at all. Add your production server’s public IP in the merchant panel. Requests from a browser or an unlisted IP are rejected.

The redirect said success but my order still shows unpaid — why?

The redirect is a browser event and can be tampered with or interrupted. Never fulfil an order from the redirect alone. Confirm the final state server-side with GET /api/v1/payments/{id} (or wait for the IPN) before marking the order paid.

How do refunds work?

A merchant requests a refund from the merchant panel. An admin reviews and issues it via POST /api/v1/admin/payments/{id}/refund. You then track the outcome — pendingsucceeded or failed — through GET /api/v1/refunds. See the Refund Flow.

Does the IPN include a signature I can verify?

The IPN is a plain JSON POST and is not cryptographically signed today. Treat it as a notification, not proof. The authoritative check is to call GET /api/v1/payments/{payment_id} and read status from the response before fulfilling. See IPN best practices.

What amount format does the API expect?

Amounts are positive decimal strings"1500.00", not the integer 150000. The only supported currency is BDT.


API Reference

Every endpoint in v1, with the authentication each one requires. Paths are relative to https://api.paperlessltd.com.

MethodEndpointDescriptionAuth
POST/api/v1/payments/initiateCreate a payment session and get a checkout URL.App Key + Secret
GET/api/v1/payments/{id}Get full details / status of a single payment.Public
GET/api/v1/paymentsList your payments (paginated, filterable).JWT Bearer
GET/api/v1/payments/{id}/logsStatus-change history for a payment.JWT Bearer
GET/api/v1/payments/{id}/timelineCustomer journey / funnel for a payment.JWT Bearer
GET/api/v1/refundsList refunds (merchant sees own, admin sees all).JWT Bearer
GET/api/v1/refunds/{id}Get a single refund’s details.JWT Bearer
POST/api/v1/admin/payments/{id}/refundIssue a refund on a payment (admin only).JWT Bearer (admin)
Interactive specA live Swagger UI for the payment core is served at /swagger/index.html on the API host, and the OpenAPI document powers this guide.