BankPay.

Overview

Everything you need to call BankPay's Provider APIs from your own systems.

All Provider APIs are reached under a common base URL and share the same authentication scheme (see Authentication below). Each API is documented as its own section further down this page - start with BIN Checker, the first API available on this platform.

Base URLhttps://bank-pay.in
Content typeapplication/json for all request and response bodies
AccessRequires a BankPay-issued API key & secret - see how to get access
Confirm your host If you were given a different API host or environment URL by your BankPay contact (e.g. a dedicated integration/UAT environment), use that instead of the base URL above.

Response envelope

Every endpoint returns JSON with at least a success/Success boolean and a message field. Field names are returned exactly as documented per endpoint below - note that some fields use PascalCase and some use lowerCamelCase depending on the endpoint; match the casing shown in each response example rather than assuming one convention throughout.

Authentication

Shared by every Provider API on this platform - API Key + HMAC-SHA256 request signing.

Each request must include three headers. There is no session/cookie auth and no OAuth flow - every single request is independently signed.

HeaderDescription
X-Api-KeyYour public API key - generate one yourself from "API Credentials" in your retailer portal.
X-TimestampCurrent Unix time in seconds. Must be within 5 minutes of BankPay's server time or the request is rejected (replay protection).
X-SignatureLowercase hex HMAC-SHA256 signature of the request, using your API secret (see below).

How to compute the signature

Build this exact string (four parts joined by newline characters), then HMAC-SHA256 it with your API secret:

{METHOD}\n{PATH}\n{TIMESTAMP}\n{BODY}
METHODThe HTTP method in upper case, e.g. POST.
PATHThe request path only (no host, no query string), e.g. /api/ProviderAPI/bincheck.
TIMESTAMPThe exact same value you send in the X-Timestamp header.
BODYThe exact raw request body as bytes - see the warning below.
Sign the exact bytes you send - not a re-serialized copy The signature is computed over the literal body bytes, including whitespace and formatting. If you build a JSON string, sign it, and then let your HTTP client re-encode or reformat the body before sending (common with some HTTP libraries' "json" helpers, or hand-editing a request in a tool like Postman after signing), the bytes you send will differ from the bytes you signed and you'll get a signature mismatch. Always send the identical string you signed - the code samples below do this correctly.

Sample code

Each sample signs and sends a BIN Checker request. Replace the placeholder key/secret with your own.

API_KEY="pk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
API_SECRET="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

METHOD="POST"
PATH="/api/ProviderAPI/bincheck"
TIMESTAMP=$(date +%s)
BODY='{"bin":"411111","latitude":"12.9716","longitude":"77.5946"}'

MESSAGE="${METHOD}
${PATH}
${TIMESTAMP}
${BODY}"

SIGNATURE=$(printf '%s' "$MESSAGE" | openssl dgst -sha256 -hmac "$API_SECRET" | sed 's/^.* //')

curl -X POST "https://bank-pay.in${PATH}" \
  -H "Content-Type: application/json" \
  -H "X-Api-Key: ${API_KEY}" \
  -H "X-Timestamp: ${TIMESTAMP}" \
  -H "X-Signature: ${SIGNATURE}" \
  --data-raw "$BODY"
// Node.js (server-side only - never run signing code like this in a browser,
// it would expose your API secret to anyone viewing the page).
const crypto = require("crypto");
const https = require("https");

const apiKey = "pk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
const apiSecret = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";

const method = "POST";
const path = "/api/ProviderAPI/bincheck";
const timestamp = Math.floor(Date.now() / 1000).toString();
const body = JSON.stringify({ bin: "411111", latitude: "12.9716", longitude: "77.5946" });

const message = `${method}\n${path}\n${timestamp}\n${body}`;
const signature = crypto.createHmac("sha256", apiSecret).update(message).digest("hex");

const options = {
    hostname: "bank-pay.in",
    path,
    method,
    headers: {
        "Content-Type": "application/json",
        "Content-Length": Buffer.byteLength(body),
        "X-Api-Key": apiKey,
        "X-Timestamp": timestamp,
        "X-Signature": signature,
    },
};

const req = https.request(options, (res) => {
    let data = "";
    res.on("data", (chunk) => (data += chunk));
    res.on("end", () => console.log(res.statusCode, data));
});
req.write(body); // same "body" string used for signing above
req.end();
import hmac, hashlib, time, json, requests

api_key = "pk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
api_secret = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

method = "POST"
path = "/api/ProviderAPI/bincheck"
timestamp = str(int(time.time()))
body = json.dumps({"bin": "411111", "latitude": "12.9716", "longitude": "77.5946"})

message = f"{method}\n{path}\n{timestamp}\n{body}"
signature = hmac.new(api_secret.encode(), message.encode(), hashlib.sha256).hexdigest()

headers = {
    "Content-Type": "application/json",
    "X-Api-Key": api_key,
    "X-Timestamp": timestamp,
    "X-Signature": signature,
}

# Use data=body (the exact signed string), NOT json=... - "json=" would let
# requests re-serialize the dict itself, which may not match what you signed.
resp = requests.post("https://bank-pay.in" + path, data=body, headers=headers)
print(resp.status_code, resp.json())
using System;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        string apiKey = "pk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
        string apiSecret = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";

        string method = "POST";
        string path = "/api/ProviderAPI/bincheck";
        string timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
        string body = "{\"bin\":\"411111\",\"latitude\":\"12.9716\",\"longitude\":\"77.5946\"}";

        string message = $"{method}\n{path}\n{timestamp}\n{body}";
        string signature;
        using (var hmacsha = new HMACSHA256(Encoding.UTF8.GetBytes(apiSecret)))
        {
            byte[] hash = hmacsha.ComputeHash(Encoding.UTF8.GetBytes(message));
            signature = BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
        }

        using (var client = new HttpClient())
        {
            client.DefaultRequestHeaders.Add("X-Api-Key", apiKey);
            client.DefaultRequestHeaders.Add("X-Timestamp", timestamp);
            client.DefaultRequestHeaders.Add("X-Signature", signature);

            // Same "body" string used for signing above - not rebuilt here.
            var content = new StringContent(body, Encoding.UTF8, "application/json");
            var response = await client.PostAsync("https://bank-pay.in" + path, content);
            string responseBody = await response.Content.ReadAsStringAsync();
            Console.WriteLine((int)response.StatusCode + " " + responseBody);
        }
    }
}

Error Handling

How failures are reported, and why some "failures" come back as HTTP 200.

Business-level outcomes (like "this BIN could not be identified") are not the same as request errors (like "this signature is invalid"). BankPay's Provider APIs distinguish the two on purpose:

HTTP StatusMeaning
200 OKThe request itself was valid and authenticated. The body's success field tells you whether the underlying lookup succeeded - a business "not found" or "could not verify" result is still a 200, and is not billed.
400 Bad RequestA required field was missing or invalid (e.g. bin isn't exactly 6 digits, latitude/longitude missing or out of range).
401 UnauthorizedMissing/invalid X-Api-Key, expired/invalid X-Timestamp, a revoked key, or a signature mismatch.
403 ForbiddenThis request came from an IP that isn't registered for your account (or no IP has been registered yet). See IP Allowlisting.
503 Service UnavailableThis API isn't currently enabled/priced for your account. Contact BankPay support.
500 Internal Server ErrorAn unexpected error on BankPay's side. Safe to retry after a short delay.
Always check the body, not just the status code A 200 response can still mean "no result" - check success (or Success, per the endpoint's documented casing) before treating a call as successful.

IP Allowlisting Required

Every key must have at least one server IP registered before it can be used.

Your API key does not work from any IP until at least one of your server IP address(es) is registered against your account - single IPs or CIDR ranges (e.g. 203.0.113.0/24) are both supported, up to a maximum of 2 active entries per account. Add your own IP(s) from "API Credentials" in your retailer portal at the same time you generate your key (or ask your BankPay contact to add them for you, if you'd rather). Any request from an IP that isn't on your account's list is rejected with 403 Forbidden, even with a valid key and signature.

New key, not yet whitelisted? If you've just generated an API key and calls are failing with 403 Forbidden, this is almost always why - add your server IP(s) from "API Credentials" in your portal, or ask your BankPay contact to confirm they've been added.

Test Mode

Integrate and test error handling before you ever touch the live upstream provider.

Every API key is issued as either Live (prefixed pk_live_) or Test (prefixed pk_test_). Generate a test key the same way you'd generate a live one, from "API Credentials" in your retailer portal - a test key still requires an existing retailer account; there is no anonymous or self-serve sandbox access without one. Live and Test access are enabled independently per API product - the Test key button only appears once you've been enabled for Test mode on at least one product, same for Live.

A test key goes through the exact same authentication and IP allowlist checks as a live key - the same request signing described in Authentication, and the same registered IP(s) from IP Allowlisting (the allowlist is keyed to your account, not to a specific key, so one registered IP covers both your live and test keys). Field validation (e.g. bin format, latitude/longitude range) is enforced identically too.

What's different: a test-key call never reaches the real upstream provider and is never charged - it returns a canned response based on the bin you send, so you can exercise both the success and the "not found" paths deterministically:

Bin sentResponse
Any 6-digit bin other than 400000Always a canned Success: true response, in the same shape documented under BIN Checker.
400000 (reserved)Always a canned Success: false, "BIN not found or could not be verified." response - use this to test your error-handling path.
RequestId is still real Test-mode responses include a genuine, uniquely generated RequestId (prefixed TBC rather than PBC), so you can still quote it if you need help - it just never corresponds to a real InstantPay call or charge.

BIN Checker Live

Look up issuer and card details for a card BIN.

POST/api/ProviderAPI/bincheck

Request body

FieldTypeRequiredNotes
binstringYesExactly 6 numeric digits. Anything shorter, longer, or non-numeric is rejected.
latitudestringYesDecimal degrees, between -90 and 90.
longitudestringYesDecimal degrees, between -180 and 180.
{
  "bin": "411111",
  "latitude": "12.9716",
  "longitude": "77.5946"
}

Response fields

Field names below are returned exactly as shown - note the mix of PascalCase and lowerCamelCase.

FieldTypeDescription
SuccessbooleanWhether a BIN match was found.
MessagestringPresent on failure - the specific reason (e.g. "BIN not found", a validation error, or an upstream provider message).
RequestIdstringUnique reference for this call - quote this if contacting support about a specific request.
ChargeAmountnumberAmount billed for this call. Only present/non-zero on a successful, billable lookup.
BankName, CardNetworkstringIssuer bank name and card network (e.g. VISA, Mastercard).
binstringThe BIN that was looked up.
cardType, cardLevelstringE.g. "Credit"/"Debit", "Platinum"/"Classic".
isoCountryName, isoCountryA2stringIssuing country name and ISO 2-letter code.
issuerBank, issuerWebsite, issuerPhonestringIssuer bank contact details, where available.
cardTransferstringUpstream-provided transfer capability detail.
{
  "Success": true,
  "Message": null,
  "RequestId": "PBC0000000123",
  "BankName": "HDFC Bank",
  "CardNetwork": "VISA",
  "ChargeAmount": 0.50,
  "bin": "411111",
  "cardType": "Credit",
  "cardLevel": "Platinum",
  "isoCountryName": "India",
  "isoCountryA2": "IN",
  "issuerBank": "HDFC Bank",
  "issuerWebsite": "https://www.hdfcbank.com",
  "issuerPhone": "1800-xxx-xxxx",
  "cardTransfer": "..."
}
{
  "Success": false,
  "Message": "BIN not found or could not be verified.",
  "RequestId": "PBC0000000124",
  "BankName": null,
  "CardNetwork": null,
  "ChargeAmount": null
}
{
  "Success": false,
  "Message": "\"bin\" must be exactly 6 numeric digits."
}
Billing You're only charged for a call where Success is true. Validation errors, auth failures, and "not found" results are never billed.

More APIs Coming soon

Additional provider APIs will be documented here as they become available, using the same authentication, error handling, and billing model described above.