Copied to clipboard
v1.0 Prerequisites API Webhooks
Developer Documentation

Build payments in minutes, not days.

Welcome. You're here because you need to accept payments in your application — and you want it done cleanly, securely, and without the usual headache. RoninPay is a developer-first payment gateway built for the modern web. It gives you a single API endpoint to create a payment, a hosted checkout page your customer completes, and real-time webhooks that tell you exactly what happened. No complex SDKs to install. No merchant accounts to configure separately. Just your credentials, a correctly-signed request, and you're live.

BASE URL http://api.rpy.life/api/v1
Protocol
HTTPS / REST
Request Format
JSON
Auth Method
HMAC-SHA256
Currency
INR
API Version
v1
Idempotency
Supported
Before You Begin

Get your gear ready

Think of this as your checklist before the journey starts. Everything below needs to be in hand before you write a single line of integration code. Skip any of these and you'll hit a wall.

  • A RoninPay merchant account — Sign up at the merchant portal. This gives you access to the dashboard where your credentials live.
  • Your Merchant ID — A UUID that identifies your business (e.g. a5973138-8f7b-4255-ae5e-81a71112b5ec). Find it under Settings in the merchant panel.
  • Your API Key — A secret key starting with sk_live_. Go to API & Webhooks in the merchant panel and generate one. Keep this private — it never goes in frontend code.
  • A server-side backend — Node.js, Python, PHP, or anything that can make HTTP requests. You cannot call this API from a browser — the signing logic requires your secret key, which must stay on the server.
  • A public webhook URL — A live HTTPS endpoint on your server where RoninPay can POST payment events. For local development, use ngrok to expose your localhost.

Store your Merchant ID and API Key in environment variables (.env). Never hardcode them in source files or commit them to version control.

The Big Picture

How a payment flows

Before diving into code, understand the full journey a payment takes — from your server calling the API, to your customer completing payment, to your server receiving confirmation. There are three actors: your server, RoninPay, and your customer.

Your server creates payment
RoninPay returns payment URL
Redirect customer to checkout
Customer pays on RoninPay
Webhook fires to your server

Step 1 is the only API call you make. Everything else — the payment UI, card processing, fraud checks — RoninPay handles. Your job after Step 1 is to: redirect the customer, then listen for the webhook that tells you the outcome.

Developer Tip

Do not rely on the redirectUrl alone to confirm a payment. Customers can close their browser mid-flow. Always trust the webhook as the source of truth for payment status.

Authentication

Your credentials, explained

RoninPay uses HMAC-SHA256 request signing — not simple API key headers. This means every request you send is cryptographically signed with a unique signature that proves it came from you and hasn't been modified in transit. There are no bearer tokens to refresh, no OAuth flows to implement. Just your two credentials working together.

CredentialFormatWhere to find itKeep it secret?
Merchant ID UUID string Merchant Panel → Settings Reasonably — it's used in signatures but not alone
API Key sk_live_... Merchant Panel → API & Webhooks Absolutely. Never expose this anywhere.

Every request needs these four headers

HeaderValueWhy it exists
Content-Type application/json Tells the server your body is JSON
X-Timestamp Unix seconds (string) Prevents replay attacks — requests older than 5 minutes are rejected
X-Idempotency-Key UUID v4 Prevents duplicate payments if you retry a failed network call
X-Signature HMAC hex string Cryptographic proof the request is authentic and unmodified
Security

How to sign a request

Signing feels complex the first time. Once you understand it, it's just three lines of code that run before every API call. Here's the exact algorithm — step by step — with the reasoning behind each step so it actually makes sense.

1

Hash your raw API key with SHA-256

RoninPay never stores your raw API key in its database — it stores the SHA-256 hash of it. This means the secret used to create your HMAC signature isn't your raw key, it's the hash of your key. This is a security design: even if the database were compromised, an attacker couldn't reconstruct your real API key.

const crypto = require('crypto');

// Your raw API key from the merchant panel
const apiKey     = process.env.RONIN_API_KEY;

// Hash it — this is what the backend uses as the HMAC secret
const apiKeyHash = crypto
  .createHash('sha256')
  .update(apiKey)
  .digest('hex');
import hashlib
import os

# Your raw API key from the merchant panel
api_key      = os.environ['RONIN_API_KEY']

# Hash it — this is what the backend uses as the HMAC secret
api_key_hash = hashlib.sha256(api_key.encode()).hexdigest()
// Your raw API key from the merchant panel
$apiKey     = getenv('RONIN_API_KEY');

// Hash it — this is what the backend uses as the HMAC secret
$apiKeyHash = hash('sha256', $apiKey);

Cache the hash — you only need to compute it once per process startup, not per request. It'll always produce the same output for the same API key.

2

Build the string-to-sign

Concatenate three things in exact order with no separator between them: your Merchant ID, the current Unix timestamp in seconds, and the full JSON body as a string. This ties the signature to your identity, a specific moment in time, and the exact request body — meaning altering any of these three things will make the signature invalid.

const merchantId = process.env.RONIN_MERCHANT_ID;
const timestamp  = Math.floor(Date.now() / 1000); // Unix seconds
const payload    = JSON.stringify(requestBody);   // The JSON body string

// Concatenate with NO separators — order matters exactly
const stringToSign = merchantId + timestamp + payload;
import time, json

merchant_id    = os.environ['RONIN_MERCHANT_ID']
timestamp      = int(time.time())          # Unix seconds
payload        = json.dumps(request_body, separators=(',',':'))

# Concatenate with NO separators — order matters exactly
string_to_sign = merchant_id + str(timestamp) + payload
$merchantId   = getenv('RONIN_MERCHANT_ID');
$timestamp    = time();                           // Unix seconds
$payload      = json_encode($requestBody);

// Concatenate with NO separators — order matters exactly
$stringToSign = $merchantId . $timestamp . $payload;

The JSON body you sign and the JSON body you send in the request must be identical. Do not re-serialize the object after signing — store the string and send it as-is.

3

Generate the HMAC-SHA256 signature

Use the hashed API key from Step 1 as the HMAC secret, and sign the string from Step 2. The result is a hex-encoded digest — that's your signature.

// Use the HASH as the secret — NOT the raw API key
const signature = crypto
  .createHmac('sha256', apiKeyHash)
  .update(stringToSign)
  .digest('hex');
import hmac

# Use the HASH as the secret — NOT the raw API key
signature = hmac.new(
  api_key_hash.encode(),
  string_to_sign.encode(),
  'sha256'
).hexdigest()
// Use the HASH as the secret — NOT the raw API key
$signature = hash_hmac('sha256', $stringToSign, $apiKeyHash);
4

Attach all four headers to your request

You now have everything you need. Add the four required headers to every API call you make.

const headers = {
  'Content-Type':       'application/json',
  'X-Timestamp':        timestamp.toString(),
  'X-Idempotency-Key':  crypto.randomUUID(),
  'X-Signature':        signature,
};
import uuid

headers = {
  'Content-Type':      'application/json',
  'X-Timestamp':       str(timestamp),
  'X-Idempotency-Key': str(uuid.uuid4()),
  'X-Signature':       signature,
}
$headers = [
  'Content-Type: application/json',
  'X-Timestamp: '       . $timestamp,
  'X-Idempotency-Key: ' . wp_generate_uuid4(), // or use ramsey/uuid
  'X-Signature: '       . $signature,
];

Generate a fresh timestamp and idempotency key for every new payment. The timestamp must be within 5 minutes of server time or the request will be rejected with a 401.

API Reference

Endpoints

RoninPay's payment API is intentionally minimal — one endpoint to create a payment, one to check its status. Complexity lives in the authentication layer, not the API surface.

POST /payments Create a payment session

This is the only API call you need to initiate a payment. Send a signed request with your payment details, and RoninPay responds with a paymentUrl. Redirect your customer there — they complete the payment on RoninPay's hosted page and land on your redirectUrl when done.

Request body parameters

FieldTypeRequiredDescription
amount number Required Amount in INR as an integer. 200 means ₹200. No paise fractions.
currency string Required Must be "INR" — only Indian Rupees are supported currently.
reference string Required Your internal order ID. Use this to match the webhook event back to the order in your database.
redirectUrl string Required Where to send the customer after payment (success or failure). Must be a valid HTTPS URL.
description string Optional A human-readable note shown on the checkout page (e.g. "Premium Plan - 1 month").

Full example — Node.js

const crypto = require('crypto');

const MERCHANT_ID = process.env.RONIN_MERCHANT_ID;
const API_KEY     = process.env.RONIN_API_KEY;
const BASE_URL    = 'http://api.rpy.life/api/v1';

async function createPayment(order) {
  const timestamp = Math.floor(Date.now() / 1000);
  const body = {
    amount:      order.amount,
    currency:    'INR',
    reference:   order.id,
    redirectUrl: order.returnUrl,
    description: order.description,
  };

  const bodyStr    = JSON.stringify(body);
  const keyHash    = crypto.createHash('sha256').update(API_KEY).digest('hex');
  const signature  = crypto
    .createHmac('sha256', keyHash)
    .update(MERCHANT_ID + timestamp + bodyStr)
    .digest('hex');

  const res = await fetch(`${BASE_URL}/payments`, {
    method:  'POST',
    headers: {
      'Content-Type':       'application/json',
      'X-Timestamp':        String(timestamp),
      'X-Idempotency-Key':  crypto.randomUUID(),
      'X-Signature':        signature,
    },
    body: bodyStr,
  });

  return res.json();
}

// Usage
const result = await createPayment({
  amount:      499,
  id:          'ORDER_' + Date.now(),
  returnUrl:   'https://yoursite.com/payment/done',
  description: 'Premium Plan - 1 month',
});

if (result.success) {
  // Redirect customer to RoninPay's checkout
  res.redirect(result.paymentUrl);
}
import os, time, hashlib, hmac, json, uuid, requests

MERCHANT_ID = os.environ['RONIN_MERCHANT_ID']
API_KEY     = os.environ['RONIN_API_KEY']
BASE_URL    = 'http://api.rpy.life/api/v1'

def create_payment(amount, order_id, redirect_url, description=''):
    timestamp = int(time.time())
    body = {
        'amount':      amount,
        'currency':    'INR',
        'reference':   order_id,
        'redirectUrl': redirect_url,
        'description': description,
    }
    body_str = json.dumps(body, separators=(',', ':'))
    key_hash = hashlib.sha256(API_KEY.encode()).hexdigest()
    signature = hmac.new(
        key_hash.encode(),
        (MERCHANT_ID + str(timestamp) + body_str).encode(),
        'sha256'
    ).hexdigest()

    response = requests.post(
        f'{BASE_URL}/payments',
        headers={
            'Content-Type':       'application/json',
            'X-Timestamp':        str(timestamp),
            'X-Idempotency-Key':  str(uuid.uuid4()),
            'X-Signature':        signature,
        },
        data=body_str,
    )
    return response.json()

# Usage
result = create_payment(499, 'ORDER_001', 'https://yoursite.com/done', 'Premium Plan')
if result['success']:
    redirect(result['paymentUrl'])  # redirect the user
<?php

function createPayment($amount, $orderId, $redirectUrl, $description = '') {
    $merchantId = getenv('RONIN_MERCHANT_ID');
    $apiKey     = getenv('RONIN_API_KEY');
    $baseUrl    = 'http://api.rpy.life/api/v1';
    $timestamp  = time();

    $body = json_encode([
        'amount'      => $amount,
        'currency'    => 'INR',
        'reference'   => $orderId,
        'redirectUrl' => $redirectUrl,
        'description' => $description,
    ]);

    $keyHash   = hash('sha256', $apiKey);
    $signature = hash_hmac('sha256', $merchantId.$timestamp.$body, $keyHash);

    $ch = curl_init($baseUrl.'/payments');
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => $body,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER     => [
            'Content-Type: application/json',
            'X-Timestamp: '       . $timestamp,
            'X-Idempotency-Key: ' . sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
                mt_rand(0,0xffff),mt_rand(0,0xffff),mt_rand(0,0xffff),
                mt_rand(0,0x0fff)|0x4000,mt_rand(0,0x3fff)|0x8000,
                mt_rand(0,0xffff),mt_rand(0,0xffff),mt_rand(0,0xffff)),
            'X-Signature: '       . $signature,
        ],
    ]);
    $result = json_decode(curl_exec($ch), true);
    curl_close($ch);
    return $result;
}

// Usage
$result = createPayment(499, 'ORDER_001', 'https://yoursite.com/done', 'Premium Plan');
if ($result['success']) {
    header('Location: ' . $result['paymentUrl']);
}
curl -X POST 'http://api.rpy.life/api/v1/payments' \
  -H 'Content-Type: application/json' \
  -H 'X-Timestamp: 1717000000' \
  -H 'X-Idempotency-Key: a3f4d2e1-8c7b-4f5a-9b2e-1d3c5f7a8e9b' \
  -H 'X-Signature: <computed_signature>' \
  -d '{
    "amount": 499,
    "currency": "INR",
    "reference": "ORDER_001",
    "redirectUrl": "https://yoursite.com/payment/done",
    "description": "Premium Plan - 1 month"
  }'

Response

200 — Success
401 — Auth Failed
JSON Response
{
  "success":    true,
  "paymentId":  "pay_abc123xyz456",
  "paymentUrl": "https://pay.rpy.life/checkout/pay_abc123xyz456",
  "reference":  "ORDER_001",
  "amount":     499,
  "currency":   "INR",
  "status":     "pending",
  "createdAt":  "2024-05-30T10:00:00.000Z"
}
JSON Response
{
  "success": false,
  "error":   "Invalid signature",
  "code":    "AUTH_FAILED"
}

Store the paymentId in your database alongside the order. You'll need it to look up payment status and to match incoming webhook events.

GET /payments/:paymentId Fetch payment status

Poll this endpoint if you need to check the current status of a payment outside of webhooks — for example when the customer lands on your redirectUrl and you want to confirm their payment before showing a success page.

Path parameters

ParameterTypeDescription
paymentId string The paymentId returned when you created the payment (e.g. pay_abc123xyz456)
async function getPayment(paymentId) {
  const timestamp = Math.floor(Date.now() / 1000);
  const keyHash   = crypto.createHash('sha256').update(API_KEY).digest('hex');
  // For GET requests, body is empty string
  const signature = crypto
    .createHmac('sha256', keyHash)
    .update(MERCHANT_ID + timestamp + '')
    .digest('hex');

  const res = await fetch(
    `${BASE_URL}/payments/${paymentId}`,
    {
      headers: {
        'X-Timestamp':       String(timestamp),
        'X-Idempotency-Key': crypto.randomUUID(),
        'X-Signature':       signature,
      }
    }
  );
  return res.json();
}
def get_payment(payment_id):
    timestamp = int(time.time())
    key_hash  = hashlib.sha256(API_KEY.encode()).hexdigest()
    # For GET requests, body is empty string
    signature = hmac.new(
        key_hash.encode(),
        (MERCHANT_ID + str(timestamp) + '').encode(),
        'sha256'
    ).hexdigest()

    response = requests.get(
        f'{BASE_URL}/payments/{payment_id}',
        headers={
            'X-Timestamp':       str(timestamp),
            'X-Idempotency-Key': str(uuid.uuid4()),
            'X-Signature':       signature,
        }
    )
    return response.json()
function getPayment($paymentId) {
    $merchantId = getenv('RONIN_MERCHANT_ID');
    $apiKey     = getenv('RONIN_API_KEY');
    $timestamp  = time();
    $keyHash    = hash('sha256', $apiKey);
    // For GET requests, body is empty string
    $signature  = hash_hmac('sha256', $merchantId.$timestamp.'', $keyHash);

    $ch = curl_init("http://api.rpy.life/api/v1/payments/$paymentId");
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER     => [
            'X-Timestamp: '       . $timestamp,
            'X-Idempotency-Key: uuid-here',
            'X-Signature: '       . $signature,
        ],
    ]);
    return json_decode(curl_exec($ch), true);
}

Payment status values

StatusMeaning
pendingPayment created, customer has not yet completed checkout
successPayment completed and confirmed — safe to fulfil the order
failedPayment attempt failed — customer can try again with a new payment
refundedRefund has been issued for this payment
Real-time Events

Webhooks — your most important tool

A webhook is a POST request that RoninPay sends to a URL on your server the moment something happens to a payment. It's faster and more reliable than polling the GET endpoint. Think of it as RoninPay calling you, rather than you calling RoninPay. This is how you actually know if a payment succeeded.

Setup

Log in to the Merchant Panel, go to API & Webhooks, find the Webhook section, and click Setup. Enter your server's public HTTPS URL (e.g. https://yoursite.com/webhooks/ronin) and save. RoninPay will immediately start sending events there.

Event types

payment.success
The customer completed payment. This is your trigger to fulfil the order, send a confirmation email, and mark the order as paid in your database.
payment.failed
The payment attempt did not go through — card declined, timeout, or customer abandoned checkout. Release any held inventory and notify the customer.
payment.pending
Payment initiated but awaiting action from the customer. Useful for showing a "payment in progress" state in your UI.
payment.refunded
A refund was issued from the merchant panel. Update your records and notify the customer that their money is on the way back.

Webhook payload shape

JSON Payload
{
  "type":      "payment.success",   // The event type
  "paymentId": "pay_abc123xyz456",   // RoninPay's payment ID
  "reference": "ORDER_001",           // Your original order ID
  "amount":    499,                   // Amount in INR
  "currency":  "INR",
  "status":    "success",
  "timestamp": 1717000980,           // Unix timestamp of the event
  "signature": "3a7f8c...d4e2"        // Verify this — see below
}

Webhook receiver — complete example

const express = require('express');
const crypto  = require('crypto');
const app     = express();

app.post('/webhooks/ronin', express.json(), (req, res) => {
  // Step 1: Respond 200 immediately — do NOT wait for processing
  res.status(200).json({ received: true });

  const event = req.body;

  // Step 2: Verify the webhook signature
  const keyHash  = crypto.createHash('sha256').update(process.env.RONIN_API_KEY).digest('hex');
  const strToVerify = process.env.RONIN_MERCHANT_ID + event.timestamp + JSON.stringify(event);
  const expected = crypto.createHmac('sha256', keyHash).update(strToVerify).digest('hex');

  if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(event.signature))) {
    console.error('Invalid webhook signature — ignoring');
    return;
  }

  // Step 3: Handle the event
  switch (event.type) {
    case 'payment.success':
      // Mark order paid, send confirmation email, release goods
      markOrderPaid(event.reference, event.paymentId);
      break;

    case 'payment.failed':
      // Release held inventory, notify customer
      handlePaymentFailed(event.reference);
      break;

    case 'payment.refunded':
      handleRefund(event.reference);
      break;
  }
});
from flask import Flask, request, jsonify
import hashlib, hmac, json, os

app = Flask(__name__)

@app.route('/webhooks/ronin', methods=['POST'])
def webhook_handler():
    event = request.get_json()

    # Verify signature
    key_hash = hashlib.sha256(os.environ['RONIN_API_KEY'].encode()).hexdigest()
    str_to_verify = os.environ['RONIN_MERCHANT_ID'] + str(event['timestamp']) + json.dumps(event, separators=(',',':'))
    expected = hmac.new(key_hash.encode(), str_to_verify.encode(), 'sha256').hexdigest()

    if not hmac.compare_digest(expected, event.get('signature', '')):
        return jsonify({'error': 'invalid signature'}), 401

    # Respond 200 first, then process
    event_type = event.get('type')
    if event_type == 'payment.success':
        mark_order_paid(event['reference'], event['paymentId'])
    elif event_type == 'payment.failed':
        handle_payment_failed(event['reference'])
    elif event_type == 'payment.refunded':
        handle_refund(event['reference'])

    return jsonify({'received': True})
<?php

$body  = file_get_contents('php://input');
$event = json_decode($body, true);

// Verify signature
$keyHash      = hash('sha256', getenv('RONIN_API_KEY'));
$strToVerify  = getenv('RONIN_MERCHANT_ID') . $event['timestamp'] . json_encode($event);
$expected     = hash_hmac('sha256', $strToVerify, $keyHash);

if (!hash_equals($expected, $event['signature'] ?? '')) {
    http_response_code(401);
    exit('Invalid signature');
}

// Respond 200 then process
http_response_code(200);
echo json_encode(['received' => true]);
fastcgi_finish_request(); // flush response before processing

switch ($event['type']) {
    case 'payment.success':
        markOrderPaid($event['reference'], $event['paymentId']);
        break;
    case 'payment.failed':
        handlePaymentFailed($event['reference']);
        break;
    case 'payment.refunded':
        handleRefund($event['reference']);
        break;
}
Security

Why verify webhooks?

Anyone on the internet can POST a fake payment.success event to your webhook URL. If you don't verify the signature, you could mark orders as paid when no real payment happened. Always validate the signature field using the same HMAC algorithm you use for outgoing requests. The code is already included in the webhook receiver example above — use crypto.timingSafeEqual (not ===) to prevent timing attacks.

Never use a plain === or == comparison to check signatures. These are vulnerable to timing attacks. Always use crypto.timingSafeEqual (Node.js), hmac.compare_digest (Python), or hash_equals (PHP).

Reference

Error codes

Every error response has the same shape: success: false, a human-readable error string, and a machine-readable code. Use the code field to handle errors programmatically.

200 Success. Request was accepted and processed. Check the success field in the response body to confirm.
400 Bad Request. A required field is missing or has an invalid value. Check the error message — it will name the offending field.
401 Unauthorized. Signature is invalid, timestamp is more than 5 minutes old, or your API key is inactive. Regenerate your signature and try again.
409 Conflict. You sent a request with an X-Idempotency-Key that was already used. This means the original request succeeded — check the payment status instead of retrying.
500 Server Error. Something went wrong on RoninPay's side. Wait a moment and retry. If this persists, contact support.

Error response shape

JSON
{
  "success": false,
  "error":   "Timestamp is more than 5 minutes old",
  "code":    "INVALID_TIMESTAMP"
}
Reference

Idempotency — never charge twice

Networks fail. Servers timeout. Retries happen. Without idempotency, retrying a failed payment request could create two separate payments. The X-Idempotency-Key header is your safety net: if RoninPay receives two requests with the same key, it returns the original response without creating a duplicate payment.

The Rule

Use a fresh UUID for every new payment. If you're retrying the exact same payment after a timeout (not a new order, the same one), reuse the same idempotency key. The server will return the original result without charging again.

Node.js
// New order → new key every time
const idempotencyKey = crypto.randomUUID();
// → "a3f4d2e1-8c7b-4f5a-9b2e-1d3c5f7a8e9b"

// Retrying same order after timeout → reuse the key you generated first
const existingKey = 'a3f4d2e1-8c7b-4f5a-9b2e-1d3c5f7a8e9b'; // saved from first attempt

// RoninPay returns HTTP 409 on duplicate → check existing payment status
Plug and Play

Complete integration module

Copy this entire file into your project. Set your environment variables and you're ready to accept payments. All the signing logic is encapsulated — you just call createPayment().

// roninpay.js — drop this in your project
// .env: RONIN_MERCHANT_ID=... RONIN_API_KEY=sk_live_...

const crypto = require('crypto');

const MERCHANT_ID = process.env.RONIN_MERCHANT_ID;
const API_KEY     = process.env.RONIN_API_KEY;
const BASE_URL    = 'http://api.rpy.life/api/v1';

const _keyHash = () =>
  crypto.createHash('sha256').update(API_KEY).digest('hex');

const _sign = (timestamp, body) =>
  crypto.createHmac('sha256', _keyHash())
    .update(MERCHANT_ID + timestamp + body)
    .digest('hex');

async function createPayment({ amount, reference, redirectUrl, description = '' }) {
  const ts   = Math.floor(Date.now() / 1000);
  const body = JSON.stringify({ amount, currency: 'INR', reference, redirectUrl, description });
  const res  = await fetch(`${BASE_URL}/payments`, {
    method: 'POST',
    headers: {
      'Content-Type':       'application/json',
      'X-Timestamp':        String(ts),
      'X-Idempotency-Key':  crypto.randomUUID(),
      'X-Signature':        _sign(ts, body),
    },
    body,
  });
  return res.json();
}

async function getPayment(paymentId) {
  const ts  = Math.floor(Date.now() / 1000);
  const res = await fetch(`${BASE_URL}/payments/${paymentId}`, {
    headers: {
      'X-Timestamp':       String(ts),
      'X-Idempotency-Key': crypto.randomUUID(),
      'X-Signature':       _sign(ts, ''),
    }
  });
  return res.json();
}

module.exports = { createPayment, getPayment };
# roninpay.py — drop this in your project
# .env: RONIN_MERCHANT_ID=... RONIN_API_KEY=sk_live_...

import os, time, hashlib, hmac, json, uuid
import requests as http

MERCHANT_ID = os.environ['RONIN_MERCHANT_ID']
API_KEY     = os.environ['RONIN_API_KEY']
BASE_URL    = 'http://api.rpy.life/api/v1'

def _key_hash():
    return hashlib.sha256(API_KEY.encode()).hexdigest()

def _sign(timestamp, body_str):
    return hmac.new(
        _key_hash().encode(),
        (MERCHANT_ID + str(timestamp) + body_str).encode(),
        'sha256'
    ).hexdigest()

def create_payment(amount, reference, redirect_url, description=''):
    ts   = int(time.time())
    body = json.dumps({
        'amount': amount, 'currency': 'INR',
        'reference': reference, 'redirectUrl': redirect_url,
        'description': description
    }, separators=(',',':'))
    return http.post(
        f'{BASE_URL}/payments',
        headers={
            'Content-Type':      'application/json',
            'X-Timestamp':       str(ts),
            'X-Idempotency-Key': str(uuid.uuid4()),
            'X-Signature':       _sign(ts, body),
        },
        data=body
    ).json()

def get_payment(payment_id):
    ts = int(time.time())
    return http.get(
        f'{BASE_URL}/payments/{payment_id}',
        headers={
            'X-Timestamp':       str(ts),
            'X-Idempotency-Key': str(uuid.uuid4()),
            'X-Signature':       _sign(ts, ''),
        }
    ).json()
<?php
// RoninPay.php — drop this in your project
// .env: RONIN_MERCHANT_ID=... RONIN_API_KEY=sk_live_...

class RoninPay {
    private static function keyHash() {
        return hash('sha256', getenv('RONIN_API_KEY'));
    }
    private static function sign($ts, $body) {
        return hash_hmac('sha256',
            getenv('RONIN_MERCHANT_ID') . $ts . $body,
            self::keyHash()
        );
    }
    private static function request($method, $path, $body='') {
        $ts  = time();
        $url = 'http://api.rpy.life/api/v1' . $path;
        $ch  = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_CUSTOMREQUEST  => $method,
            CURLOPT_POSTFIELDS     => $body,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER     => [
                'Content-Type: application/json',
                'X-Timestamp: '       . $ts,
                'X-Idempotency-Key: ' . sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
                    mt_rand(0,65535),mt_rand(0,65535),mt_rand(0,65535),
                    mt_rand(0,0x0fff)|0x4000,mt_rand(0,0x3fff)|0x8000,
                    mt_rand(0,65535),mt_rand(0,65535),mt_rand(0,65535)),
                'X-Signature: '       . self::sign($ts, $body),
            ],
        ]);
        $result = json_decode(curl_exec($ch), true);
        curl_close($ch);
        return $result;
    }
    public static function createPayment($amount, $ref, $url, $desc='') {
        $body = json_encode([
            'amount'=>$amount,'currency'=>'INR','reference'=>$ref,
            'redirectUrl'=>$url,'description'=>$desc
        ]);
        return self::request('POST', '/payments', $body);
    }
    public static function getPayment($paymentId) {
        return self::request('GET', '/payments/'.$paymentId);
    }
}

// Usage
$result = RoninPay::createPayment(499, 'ORDER_001', 'https://yoursite.com/done', 'Premium Plan');
if ($result['success']) {
    header('Location: ' . $result['paymentUrl']);
}

You're done. Set RONIN_MERCHANT_ID and RONIN_API_KEY in your environment, register your webhook URL in the merchant panel, and call createPayment() from your order flow. That's the entire integration.