Payluma

API docs

Create an order, send the customer to checkout_url (or show upi_link yourself), and get a payment.success webhook when your bank confirms the money.

1. Authentication

Create keys in Dashboard → API & webhooks. sk_test_… keys make test orders you can pay with a "Simulate" button. sk_live_… keys take real UPI payments once your account is approved.

2. Create an order

curl -X POST https://payluma.in/api/v1/orders \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 499,
    "order_id": "INV-1042",
    "description": "Pro plan",
    "customer_name": "Ravi Kumar",
    "customer_email": "[email protected]",
    "redirect_url": "https://yoursite.com/thanks",
    "notes": { "plan": "pro" }
  }'

amount is in rupees. The customer pays amount_payable: your amount plus a unique paise ending (always rounded up), which is how the bank alert identifies the order without a UTR. Sending the same order_id again returns the same order. Orders expire after 15 minutes; a payment that lands up to 10 minutes late is still matched.

{
  "id": "ord_8k2j3m9x1q4zabcd",
  "order_id": "INV-1042",
  "mode": "test",
  "status": "pending",
  "amount": 499,
  "amount_payable": 499.37,
  "checkout_url": "https://payluma.in/pay/ord_8k2j3m9x1q4zabcd",
  "upi_link": "upi://pay?pa=shop@okhdfcbank&pn=...&am=499.37&cu=INR&tr=ord_...",
  "expires_at": "2026-11-04T10:15:00.000Z",
  ...
}

3. Get the result

Set a webhook URL in the dashboard. Failed deliveries retry after 1 min, 5 min, 30 min, 2 h, 6 h and 12 h. Reply with any 2xx. Events: payment.success, payment.reverted (a manual approval was undone).

POST https://yoursite.com/payluma/webhook
Payluma-Event: payment.success
Payluma-Signature: t=1793750000,v1=5f2c...

{
  "id": "<delivery id>",
  "event": "payment.success",
  "data": {
    "order_id": "ord_8k2j3m9x1q4zabcd",
    "merchant_order_id": "INV-1042",
    "status": "paid",
    "amount": 499,
    "amount_paid": 499.37,
    "utr": "530712345678",
    "verified_by": "auto",     // or "manual" when you approved it in the dashboard
    "paid_at": "2026-11-04T10:02:11.000Z",
    ...
  }
}

Verify the signature

import crypto from "node:crypto"

// rawBody: the request body exactly as received (string), header: Payluma-Signature
function verifyPayluma(rawBody, header, secret) {
  const { t, v1 } = Object.fromEntries(header.split(",").map((part) => part.split("=")))
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false // older than 5 minutes
  const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex")
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1))
}

You can also poll GET /api/v1/orders/:id or GET /api/v1/orders?order_id=INV-1042. After payment the customer is sent to redirect_url?order_id=…&status=paid. Always confirm with the webhook or the API, not the redirect.

Errors

{ "error": { "code": "...", "message": "..." } } with 400 (invalid_request, invalid_amount), 401 (unauthorized), 403 (not_approved, billing_suspended, credit_limit), 429 (busy: too many open orders for one amount).