PayPluse API

Two endpoints, both JSON. Amounts are in INR with two decimals. Timestamps are ISO-8601. Base path: /api/v1.

Getting started

1. Connect your FamPay mailbox in the console so payments can be verified. 2. Generate an API key (or create orders from the dashboard). 3. Send customers to the checkout URL from the order response. 4. Optionally register a webhook endpoint for signed state changes.

Authentication

Order creation uses a Bearer API key from your console's API Keys page. Keys start with payplus_live_ or payplus_test_, are shown once when generated, and are stored hashed — we cannot show one again. Revocation is immediate; rotation issues a new key and revokes the old one in the same step. Order status checks need no key: holding the unguessable order id is the authorization.

Authorization header
Authorization: Bearer payplus_live_9f2c…

Create an order

POST /api/v1/orders — amount is the only required field. Everything else is optional and validated exactly like the dashboard form. Your FamPay mailbox must be connected or the call is rejected.

Request
POST /api/v1/orders
Authorization: Bearer payplus_live_…
Content-Type: application/json

{
  "amount": 499.00,
  "referenceNote": "invoice-42",
  "description": "UI kit license",
  "customerName": "Priya",
  "customerEmail": "priya@example.com",
  "customerPhone": "9876543210",
  "returnUrl": "https://yourstore.com/thanks",
  "metadata": { "plan": "pro" }
}
201 response
{
  "ok": true,
  "order": {
    "orderId": "PPM7X2K9Q1A",
    "amount": 499.00,
    "currency": "INR",
    "status": "PENDING",
    "expiresAt": "2026-09-26T10:30:00.000Z",
    "checkoutUrl": "/pay/PPM7X2K9Q1A"
  }
}

Check an order

GET /api/v1/orders/:orderId — public, no key. This is the same endpoint the checkout page polls every few seconds. Only checkout-safe fields are ever returned: no merchant ids, no customer contact details, no metadata.

200 response
{
  "ok": true,
  "order": {
    "orderId": "PPM7X2K9Q1A",
    "amount": 499.00,
    "currency": "INR",
    "status": "PAID",
    "description": "UI kit license",
    "expiresAt": "2026-09-26T10:30:00.000Z",
    "merchantName": "Aarav Store",
    "merchantAvatar": null,
    "linkStatus": null,
    "returnUrl": "https://yourstore.com/thanks"
  }
}

status is one of PENDING, PROCESSING, PAID, REVIEW, EXPIRED, CANCELLED, FAILED. Only the server-side verification engine can move an order to PAID — never a button, a redirect back from a UPI app, or a call you make.

Order statuses

PENDING → PROCESSING → PAID is the verified path. REVIEW means ambiguous evidence needs a human; FAILED means the evidence contradicted the order; EXPIRED and CANCELLED are terminal without payment. Only the server-side verification engine can move an order to PAID.

Checkout

Send the customer to /pay/:orderId. The page renders a UPI QR from a server-frozen snapshot of the order, counts down to expiry, and updates live as the server verifies the payment. After a confirmed PAID, the customer is offered a Continue button to your returnUrl if you set one.

Checkout session

POST /api/checkout/session — public, no key, 30/min per IP. Body { "orderId": "PP…" }. Returns the frozen snapshot the QR is built from: orderId, amount, currency, upiPayeeVpa, expiresAt. Unknown orders answer 404.

Webhooks

Register HTTPS endpoints in the console and pick which of these six events to receive: order.paid, order.processing, order.review, order.failed, order.expired, order.cancelled. Every delivery carries a stable event_id.

Dedupe on event_id, not on receipt. You may receive the same event_id more than once — retries reuse it, and manual retries reuse it too. Seeing an id twice is always safe to no-op.

Delivery body
{
  "event_id": "3f9d…",
  "event_type": "order.paid",
  "order_id": "PPM7X2K9Q1A",
  "amount": 499.00,
  "occurred_at": "2026-09-26T10:31:02.000Z"
}

Verifying signatures

Every delivery signs its body: header X-Signature: t=<unix_ts>,v1=<hmac> where hmac = HMAC-SHA256(secret, "<t>.<payload>"). Compare in constant time and reject stale timestamps (ten minutes is a sane window).

Node.js verification
import { createHmac, timingSafeEqual } from 'node:crypto';

function verify(secret, rawBody, header) {
  const match = /^t=(\d+),v1=([0-9a-f]+)$/.exec(header ?? '');
  if (!match) return false;
  const [, t, v1] = match;
  if (Math.abs(Date.now() / 1000 - Number(t)) > 600) return false; // replay window
  const expected = createHmac('sha256', secret).update(t + '.' + rawBody).digest();
  const received = Buffer.from(v1, 'hex');
  return received.length === expected.length && timingSafeEqual(received, expected);
}

API keys

Manage keys on the console's API Keys page. Keys start with payplus_live_ or payplus_test_, are shown once at generation, are stored hashed, revoke immediately, and rotate atomically (new key issued, old key revoked in the same step).

Rate limits

  • POST /api/v1/orders — 60/min per API key (plus a 30/min per-IP brake before auth).
  • GET /api/v1/orders/:orderId — 120/min per IP.
  • Webhook management in the console — 30/min per merchant.

Limits are per server instance in v1 and reset on deploy. A refused request returns 429 with the standard error shape and a Retry-After header in seconds.

Errors

One shape everywhere: { "error": true, "code": "...", "message": "..." }. Auth failures never reveal whether a key exists, and rate-limit responses look the same as auth failures to an outsider.

Testing

A payplus_test_ key authenticates exactly like a live key. There is no separate sandbox ledger: test keys create real orders on your account, so use low amounts and cancel test orders when you are done. Verification still needs a connected mailbox and real payment evidence — test mode never invents a payment.

Refunds

PayPluse never holds your money, so it cannot send it back: there is no refund endpoint and no refund status. A refund happens entirely in your own FamPay/UPI app, outside PayPluse.