VolvoPay API

Merchant dashboard

INR payment gateway with Pay-in (collect money via UPI / payment page) and Payout (send money to a bank account or UPI ID). JSON over HTTPS. All amounts are in INR with up to two decimals.

Base URL: https://volvopay.hmmm.digital/v1
Authentication: send your key in the x-api-key header. Keys start with vp_live_; generate yours in the dashboard → API key (one key per account, regenerate to rotate). Never put a key in client-side code.

Rate limits: 240 requests/min per account, 30/min for payouts. Errors: non-2xx with { "error": "code", "message": "..." }.

1. Create a payin

POST /v1/payin

FieldTypeRequired
amountnumberyesINR, e.g. 499 or 499.50
order_idstringnoYour own id. Makes the call idempotent: the same order_id returns the same payin (created:false) instead of a new one.
customerobjectno{ name, email, phone } — shown nowhere, stored for your reference
redirect_urlstringnoWhere the customer is sent after paying. We append payin_id, order_id and status as query params. Never trust that redirect to mark an order paid — use the webhook or GET status.
notestringnoShown on the payment page
curl -X POST https://volvopay.hmmm.digital/v1/payin \
  -H "x-api-key: vp_live_xxx" -H "Content-Type: application/json" \
  -d '{"amount": 499, "order_id": "INV-1001", "redirect_url": "https://yoursite.com/thanks",
       "customer": {"name": "Rohan", "phone": "9876543210"}}'

Response 201:

{
  "id": "ORD_1792639059438", "order_id": "INV-1001", "mode": "live",
  "amount": 499, "fee": 24.95, "net_amount": 474.05, "currency": "INR",
  "status": "pending", "utr": null,
  "payment_url": "https://volvopay.hmmm.digital/pay/ORD_1792639059438",
  "gateway_url": "https://.../cashier?orderId=...",
  "expires_at": "2026-10-22T10:30:00.000Z", "created_at": "2026-10-22T10:00:00.000Z", "created": true
}

Send the customer to payment_url (our hosted page: shows the amount, opens the UPI cashier, polls for the result, lets the customer enter a UTR if the bank is slow, then redirects). gateway_url is the raw cashier link if you prefer to open it yourself.

status is one of pending, success, failed, expired. A payin can still go from expired to success if the bank confirms late — always handle a late success webhook.

2. Get a payin

GET /v1/payin/{id} — id is our payin id (ORD_…, the same id the bank gateway uses) or your order_id. For a pending live payin this asks the bank gateway right now, so it is safe to call after the customer returns.

GET /v1/payins?status=success&limit=50&before=ORD_… — list, newest first, cursor pagination via next_before.

Manual UTR verification

POST /v1/payin/{id}/utr with { "utr": "123456789012" } — when the customer paid but the status is stuck on pending. Returns the updated payin. The hosted page already offers this to customers.

3. Create a payout

POST /v1/payout — only from a server IP you whitelisted in the dashboard → Settings. Fee is added on top of amount and the total is debited from your balance immediately; if the bank rejects the transfer the full amount comes back.

FieldTypeRequired
amountnumberyesINR the beneficiary receives
referencestringnoYour own id. Strongly recommended — the same reference never sends twice (created:false), so a network retry is safe.
beneficiaryobjectyes{ "type":"bank", "account":"987654321012", "ifsc":"HDFC0001234", "name":"Rohan Sharma" }
or { "type":"upi", "upi":"rohan@okhdfcbank" }
curl -X POST https://volvopay.hmmm.digital/v1/payout \
  -H "x-api-key: vp_live_xxx" -H "Content-Type: application/json" \
  -d '{"amount": 500, "reference": "WD-7781",
       "beneficiary": {"type":"bank","account":"987654321012","ifsc":"HDFC0001234","name":"Rohan Sharma"}}'
{
  "id": "PAYOUT_1792639059438", "reference": "WD-7781", "mode": "live",
  "amount": 500, "fee": 15, "total_debit": 515, "currency": "INR",
  "status": "processing", "utr": null, "failure_reason": null,
  "beneficiary": {"type":"bank","account":"987654321012","ifsc":"HDFC0001234","name":"Rohan Sharma"},
  "created_at": "2026-10-22T10:00:00.000Z", "created": true
}

status: processing → success (with utr) or failed (money returned to your balance). GET /v1/payout/{id} (our id or your reference), GET /v1/payouts, GET /v1/payout-quote?amount=500 (fee preview).

Possible errors: insufficient_balance (402), payout_ip_not_whitelisted (403), amount_below_minimum, invalid_ifsc, gateway_rejected (502, already refunded).

4. Balance

GET /v1/balance → { "currency":"INR", "available": 12345.50, "limits": {...}, "fees": {...}, "payout_ip_whitelist": [...], "your_ip": "1.2.3.4" }. your_ip is handy to find what to whitelist.

5. Webhooks

Set your HTTPS URL in the dashboard → Settings. We POST JSON for payin.success, payin.failed, payout.success, payout.failed, settlement.paid:

{ "event": "payin.success", "created_at": "2026-10-22T10:05:00.000Z",
  "data": { "id": "ORD_1792639059438", "order_id": "INV-1001", "amount": 499, "net_amount": 474.05, "status": "success", "utr": "326512345678", ... } }

Reply 2xx within 10 s. Anything else is retried with backoff for about 26 hours (8 attempts). Deliveries can arrive more than once — treat the data.id + data.status pair as idempotent.

Verify the signature

Header X-VolvoPay-Signature: t=1729591500,v1=<hex> where v1 = HMAC_SHA256(secret, t + "." + rawBody). The secret is in the dashboard → Settings. Reject if the signature does not match or t is older than 5 minutes.

// Node / Express — use the RAW body, not re-serialised JSON
const crypto = require('crypto');
app.post('/volvopay/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = Object.fromEntries(req.get('X-VolvoPay-Signature').split(',').map(p => p.split('=')));
  const expected = crypto.createHmac('sha256', process.env.VOLVOPAY_WEBHOOK_SECRET)
    .update(sig.t + '.' + req.body.toString()).digest('hex');
  if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig.v1 || ''))) return res.status(400).end();
  const { event, data } = JSON.parse(req.body);
  // ... mark order data.order_id paid when event === 'payin.success'
  res.json({ ok: true });
});
<?php // PHP
$raw = file_get_contents('php://input');
parse_str(str_replace(',', '&', $_SERVER['HTTP_X_VOLVOPAY_SIGNATURE']), $sig);
$expected = hash_hmac('sha256', $sig['t'] . '.' . $raw, $secret);
if (!hash_equals($expected, $sig['v1'] ?? '')) { http_response_code(400); exit; }
$event = json_decode($raw, true);

6. Settlements

Your balance (net of fees) is yours to withdraw any time from the dashboard → Settlements to your saved bank account or UPI. Requests are processed on demand by the VolvoPay team; you receive a settlement.paid webhook with the UTR. Alternatively use the payout API to move funds wherever you need.

Quick PHP example — create a payin

$ch = curl_init('https://volvopay.hmmm.digital/v1/payin');
curl_setopt_array($ch, [
  CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => ['x-api-key: vp_live_xxx', 'Content-Type: application/json'],
  CURLOPT_POSTFIELDS => json_encode(['amount' => 499, 'order_id' => 'INV-1001', 'redirect_url' => 'https://yoursite.com/thanks']),
]);
$payin = json_decode(curl_exec($ch), true);
header('Location: ' . $payin['payment_url']);

VolvoPay · API v1