Skip to content
Developers

Merchant API

UPI pay-ins from customers in India, IMPS and NEFT pay-outs to Indian bank accounts, status and balance calls, and webhooks. Everything is in INR against one merchant balance.

Base URL
https://api.payeeglobal.com
Authentication
API secret key as a Bearer token
Encryption
AES-256-CBC for pay-in and pay-out creation
Answers
Read status in the body; most errors use HTTP 200
Results
By webhook: confirm each with the status call
Testing
Small real amounts on a test account
On this page
Overview

How the API works

The API behind payeeglobal.com takes UPI pay-ins from customers in India and sends pay-outs to Indian bank accounts over IMPS and NEFT, all in INR, against one merchant balance.

  • Base URL (production): https://api.payeeglobal.com
  • Authentication: your secret key as a Bearer token on every call.
  • Encryption: creating a pay-in or a pay-out also needs the request fields encrypted with AES-256-CBC under your secret hash. The status and balance calls are plain JSON.
  • Answers: read status in the body. Most errors use HTTP 200; only authentication failures use HTTP 401.
  • Results: final outcomes arrive by webhook. Confirm each one with a status call before acting on it.

Testing uses small real amounts on a test account we set up with you.

Your API key, secret hash and IV are in the merchant console. Merchant login.

The five calls

MethodPathWhat it does
POST/v2/upi/payinCreate pay-in
POST/v2/upi/statusPay-in status
POST/v2/refundCreate pay-out
POST/v2/refund/statusPay-out status
POST/v2/transaction/balanceBalance

Guides

Guide 01

Quickstart

The API takes UPI pay-ins from customers in India and sends pay-outs to Indian bank accounts over IMPS and NEFT. Everything is in INR and settles to one merchant balance.

What you need

From the merchant console:

Value Where to find it Used for
API secret key Settings, API keys and webhooks Authorization: Bearer <API_KEY> on every call
Secret hash Settings, API keys and webhooks the AES-256 key that encrypts pay-in and pay-out requests
IV the API documentation page in the console the 16-byte initialisation vector for the same encryption

Base URL: https://api.payeeglobal.com

Keep all three on your server. They must never reach a browser or a mobile app.

  1. Check your key

The balance call needs only your API key, so it is the quickest way to confirm your setup.

Examples
curl -sS -X POST 'https://api.payeeglobal.com/v2/transaction/balance' \
  -H 'Authorization: Bearer <API_KEY>' \
  -H 'Content-Type: application/json'

A working key returns "status": "success" with your balances. A wrong key returns HTTP 401 with "status": "unauthorized".

  1. Create a pay-in

Pay-in fields are sent encrypted. The authentication guide explains the scheme; this is the whole call in cURL:

Shell
API_KEY='<API_KEY>'
SECRET_HASH='<SECRET_HASH>'   # base64, from Settings in the merchant console
IV_HEX='<IV_HEX>'             # 32 hex characters, from the API documentation page in the console

# The AES-256 key as hex, decoded from the base64 secret hash
KEY_HEX=$(printf '%s' "$SECRET_HASH" | openssl base64 -d -A | od -An -v -tx1 | tr -d ' \n')

FIELDS='{"merchant_order_id":"<merchant_order_id>","first_name":"<first_name>","last_name":"<last_name>","email":"<email>","phone_number":"<phone_number>","amount":"<amount>","membership_duration":"<membership_duration>","response_url":"<response_url>","webhook_url":"<webhook_url>"}'

ENCRYPTED=$(printf '%s' "$FIELDS" | openssl enc -aes-256-cbc -K "$KEY_HEX" -iv "$IV_HEX" -base64 -A)

curl -sS -X POST 'https://api.payeeglobal.com/v2/upi/payin' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d "{\"encrypted_data\":\"$ENCRYPTED\"}"

The same call in Node, Python, Go and PHP is in the pay-ins guide.

  1. Send the customer to pay

A created pay-in comes back with "status": "authenticate". Either redirect the customer to authenticate_url, a hosted page with a QR code and buttons for the common UPI apps, or show upi_intent yourself as a QR code or a deep link.

  1. Confirm the result

The final result reaches your webhook_url. Confirm each webhook with the status call before you release goods or credit an account:

Shell
curl -sS -X POST 'https://api.payeeglobal.com/v2/upi/status' \
  -H 'Authorization: Bearer <API_KEY>' \
  -H 'Content-Type: application/json' \
  -d '{"merchant_order_id":"<merchant_order_id>"}'

Before you launch

  • Read status in the response body. Most errors come back with HTTP 200; only authentication failures use HTTP 401.
  • Always send a unique merchant_order_id. It is how you look a pay-in up, and it stops the same order being created twice.
  • Test with small real amounts on a test account we open with you.
  • You can restrict API calls to your server addresses under IP allow-list in the console.
Guide 02

Authentication and encryption

API key

Every call carries your API secret key as a Bearer token:

Text
Authorization: Bearer <API_KEY>

You can see and reset the key under Settings, API keys and webhooks, in the merchant console. Resetting it stops the old key at once, so update your servers first.

Problem HTTP Body
No Authorization header 401 {"status": "unauthorized", "message": "Authentication failed."}
Unknown key, or API access switched off for the account 401 {"status": "unauthorized", "message": "Invalid secret key."}

Creating a pay-out answers these in the same shape. The pay-out status call answers them as {"status": 401, "message": ...}.

Encrypted requests

Creating a pay-in (POST /v2/upi/payin) and creating a pay-out (POST /v2/refund) need the request fields encrypted. The status and balance calls are plain JSON.

  1. Build the request fields as a JSON object and serialise it as UTF-8.
  2. Encrypt it with AES-256-CBC and PKCS#7 padding. The key is your secret hash, base64-decoded to 32 bytes. The IV is the hex value from the console's API documentation page, decoded to 16 bytes.
  3. Base64-encode the ciphertext (standard alphabet, with padding).
  4. Send it as the only field of a JSON body, with Content-Type: application/json:
JSON
{"encrypted_data": "<BASE64_CIPHERTEXT>"}

The older spelling encryted_data is also accepted. If the value is missing or cannot be decrypted you get HTTP 200 with "status": "validation_error" and Encrypted data must be a non-empty string. or Invalid data encryption.

The encryption step in five languages

Examples
SECRET_HASH='<SECRET_HASH>'   # base64, from Settings in the merchant console
IV_HEX='<IV_HEX>'             # 32 hex characters, from the API documentation page in the console

# The AES-256 key as hex, decoded from the base64 secret hash
KEY_HEX=$(printf '%s' "$SECRET_HASH" | openssl base64 -d -A | od -An -v -tx1 | tr -d ' \n')

# FIELDS holds the request fields as one line of JSON, for example '{"amount":"<amount>"}'
ENCRYPTED=$(printf '%s' "$FIELDS" | openssl enc -aes-256-cbc -K "$KEY_HEX" -iv "$IV_HEX" -base64 -A)

To check your ciphertext locally, decrypt it again with the same key and IV:

Shell
printf '%s' "$ENCRYPTED" | openssl enc -d -aes-256-cbc -K "$KEY_HEX" -iv "$IV_HEX" -base64 -A

It should print the JSON you started with.

Secret hash

The secret hash is set when your account is opened and is shown under Settings. It cannot be changed from the console; if you think it has been exposed, contact us.

IP allow-list

Under IP allow-list in the console you can list the server addresses allowed to call the API. When the list is on, creating a pay-in, checking a pay-in and creating a pay-out from any other address fail with HTTP 401 and IP address (<ip>) not whitelisted.

Keep keys on the server

The API key, the secret hash and the IV belong on your servers only. Never put them in a web page, a mobile app or a support ticket.

Guide 03

Pay-ins

A pay-in collects money from a customer over UPI.

Text
create pay-in  ->  status "authenticate"  ->  customer pays  ->  webhook  ->  confirm with status call  ->  fulfil

Create a pay-in

POST /v2/upi/payin, with the fields below encrypted as described in the authentication guide.

Field Required Rule
merchant_order_id No, but send it your unique reference for the order
first_name, last_name Yes 2 to 100 characters
email Yes valid email address
phone_number Yes 10 digits, first digit 6 to 9, no country code
amount Yes decimal INR, for example "100.00"
membership_duration Yes months the customer has been a member of your platform; below 3 is refused
response_url Yes full URL with a public domain; the customer returns here
webhook_url Yes full URL with a public domain; results are posted here
customer_user_id No your customer reference
upi No the customer's UPI ID, if you have it
pg_id No terminal ID, only if we issued you several
address, city, state, zip No 2 to 250 characters
country No two capital letters
Examples
API_KEY='<API_KEY>'
SECRET_HASH='<SECRET_HASH>'   # base64, from Settings in the merchant console
IV_HEX='<IV_HEX>'             # 32 hex characters, from the API documentation page in the console

# The AES-256 key as hex, decoded from the base64 secret hash
KEY_HEX=$(printf '%s' "$SECRET_HASH" | openssl base64 -d -A | od -An -v -tx1 | tr -d ' \n')

FIELDS='{"merchant_order_id":"<merchant_order_id>","first_name":"<first_name>","last_name":"<last_name>","email":"<email>","phone_number":"<phone_number>","amount":"<amount>","membership_duration":"<membership_duration>","response_url":"<response_url>","webhook_url":"<webhook_url>"}'

ENCRYPTED=$(printf '%s' "$FIELDS" | openssl enc -aes-256-cbc -K "$KEY_HEX" -iv "$IV_HEX" -base64 -A)

curl -sS -X POST 'https://api.payeeglobal.com/v2/upi/payin' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d "{\"encrypted_data\":\"$ENCRYPTED\"}"

Show the payment to the customer

A created pay-in answers:

JSON
{
  "status": "authenticate",
  "authenticate_url": "https://api.payeeglobal.com/<hosted-page>/<reference>",
  "upi_intent": "upi://pay?pa=<payee_vpa>&pn=<payee_name>&am=100.00&cu=INR&tr=<reference>",
  "merchant_order_id": "ORDER-10001",
  "transaction_id": "T4K2Q9ZB1790000000000",
  "amount": "100.00",
  "message": "QR Generated."
}

You have two ways to show it:

  • Hosted page. Redirect the customer to authenticate_url. It shows a QR code, buttons for the common UPI apps and a countdown. When it finishes, the customer is sent to your response_url with status, message, order_id and transaction_id in the query string. Use those parameters only to choose what to show, never to decide whether the order is paid.
  • Your own page. Render upi_intent as a QR code on desktop, or open it as a link on a phone so the customer's UPI app starts with the payment filled in.

"status": "authenticate" means the pay-in exists and is waiting. It is not paid yet.

Check the result

The result arrives by webhook. Confirm it, or poll if you have not heard anything, with the status call. Look pay-ins up by merchant_order_id: transaction_id is accepted but not used for the lookup.

Examples
curl -sS -X POST 'https://api.payeeglobal.com/v2/upi/status' \
  -H 'Authorization: Bearer <API_KEY>' \
  -H 'Content-Type: application/json' \
  -d '{"merchant_order_id":"<merchant_order_id>"}'

Pay-in statuses

status Meaning Webhook
authenticate created, waiting for the customer no
success paid; utr carries the bank reference when the rail returns it yes
failed declined or not completed yes
canceled the customer cancelled, or the hosted page timed out no
expired left unpaid for two days no
blocked refused before it reached a rail, for example over a limit on your account usually; not when the amount is over your per-transaction limit
on_hold held for review yes
pending, to_be_confirm rare intermediate states no
settled included in a settlement to you no

Duplicates and retries

  • A merchant_order_id you have already used is refused with Duplicate order_id, field must be unique.
  • If a create call times out, do not send it again straight away. Check the status by merchant_order_id first: the pay-in may already exist.
Guide 04

Pay-outs

A pay-out sends money from your pay-out balance to an Indian bank account over IMPS or NEFT. The path is /v2/refund for historical reasons.

Before you send

Check payout_balance with the balance call. The amount plus any fee is taken from that balance as soon as a pay-out is accepted.

Examples
curl -sS -X POST 'https://api.payeeglobal.com/v2/transaction/balance' \
  -H 'Authorization: Bearer <API_KEY>' \
  -H 'Content-Type: application/json'

Create a pay-out

POST /v2/refund, with these fields encrypted exactly as for a pay-in. Every field is required.

Field Rule
order_id your unique reference: 3 to 100 letters, digits or dashes
amount decimal INR; the minimum is 100
payment_mode IMPS or NEFT
bank_name beneficiary's bank
bank_account_number beneficiary's account number
ifsc_code IFSC of the beneficiary's branch
account_holder_name name on the account
account_type 1 savings or 2 current
customer_email valid email address
customer_phone_no 10 digits, first digit 6 to 9
remark free text
response_url, webhook_url full URLs with a public domain
Examples
API_KEY='<API_KEY>'
SECRET_HASH='<SECRET_HASH>'   # base64, from Settings in the merchant console
IV_HEX='<IV_HEX>'             # 32 hex characters, from the API documentation page in the console

# The AES-256 key as hex, decoded from the base64 secret hash
KEY_HEX=$(printf '%s' "$SECRET_HASH" | openssl base64 -d -A | od -An -v -tx1 | tr -d ' \n')

FIELDS='{"order_id":"<order_id>","amount":"<amount>","payment_mode":"IMPS","bank_name":"<bank_name>","bank_account_number":"<bank_account_number>","ifsc_code":"<ifsc_code>","account_holder_name":"<account_holder_name>","account_type":"<account_type>","customer_email":"<customer_email>","customer_phone_no":"<customer_phone_no>","remark":"<remark>","response_url":"<response_url>","webhook_url":"<webhook_url>"}'

ENCRYPTED=$(printf '%s' "$FIELDS" | openssl enc -aes-256-cbc -K "$KEY_HEX" -iv "$IV_HEX" -base64 -A)

curl -sS -X POST 'https://api.payeeglobal.com/v2/refund' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d "{\"encrypted_data\":\"$ENCRYPTED\"}"

Read the answer

Pay-out answers put a numeric code in status. It is not the HTTP status.

Body status HTTP Meaning
303 200 accepted and pending; the usual answer. The result arrives by webhook
200 200 completed
400 200 declined
300 200 needs a redirect to auth_url (rare)
402 200 refused: validation, balance, minimum amount or no pay-out terminal on your account
401 401 IP not allowed, or an order_id you have already used

A missing or unknown key answers HTTP 401 with {"status": "unauthorized", ...}, the pay-in shape.

If encrypted_data is missing or cannot be decrypted, the answer uses the pay-in shape: {"status": "validation_error", "message": "Invalid data encryption."}.

Check a pay-out

Send your order_id as merchant_order_id, or send our transaction_id or the utr.

Examples
curl -sS -X POST 'https://api.payeeglobal.com/v2/refund/status' \
  -H 'Authorization: Bearer <API_KEY>' \
  -H 'Content-Type: application/json' \
  -d '{"merchant_order_id":"<order_id>"}'

A pay-out that is not found answers {"status": 402, "message": "Transaction not found."}.

Webhook

When a pay-out becomes success or failed we post it to your webhook_url. The reference key there is order_id. See the webhooks guide.

If a call times out

Check the status by your order_id before doing anything else. Do not resend with a new order_id: that creates a second pay-out and takes the amount twice.

Guide 05

Webhooks

We post a JSON message to your webhook_url when a pay-in or a pay-out reaches a final state, and when our operations team records a refund, chargeback or flag on one of your pay-ins.

Delivery

  • POST, Content-Type: application/json, to the webhook_url you sent with the request.
  • Answer HTTP 200. Any other code, including 201 and 204, counts as a failed delivery.
  • We wait up to 60 seconds for your answer.
  • Pay-ins: a failed delivery is retried once a minute for pay-ins at least five minutes old, up to three more times.
  • Pay-outs: retried from 15 minutes after the status change, up to three more times.
  • Refund, chargeback and flag events are sent once and not retried.
  • The same status can arrive more than once. Handle each transaction_id and status pair once.
  • There is no webhook when a pay-in is canceled or expired.

Check every webhook

Treat every webhook as a prompt to check, never as proof of payment:

  1. Take merchant_order_id (or order_id for a pay-out) from the body.
  2. Ask the status call what happened.
  3. Act only on the answer from the status call, and only if its merchant_order_id matches.
  4. Use the amount from the status call, not from the webhook.

Serve your webhook URL over HTTPS and keep it out of public pages.

Pay-in webhook

JSON
{
  "status": "success",
  "merchant_order_id": "ORDER-10001",
  "transaction_id": "T4K2Q9ZB1790000000000",
  "amount": "100.00",
  "message": "<detail>",
  "upi": null,
  "utr": "000000000000"
}

status is success, failed, blocked or on_hold. upi and utr can be null or missing.

Pay-out webhook

JSON
{
  "status": "success",
  "order_id": "PAYOUT-10001",
  "transaction_id": "PX7Y2K9LM1790000000000",
  "utr": "000000000000",
  "amount": "100.00",
  "message": "<detail>"
}

status is success or failed.

Refund, chargeback and flag events

JSON
{
  "status": 200,
  "event": "refund",
  "message": "Transaction refund successfully.",
  "transaction": {
    "merchant_order_id": "ORDER-10001",
    "transaction_id": "T4K2Q9ZB1790000000000",
    "amount": "100.00",
    "message": "<detail>",
    "utr": "000000000000",
    "refund_status": true,
    "refund_date": "<date>"
  }
}

event is refund, chargeback or flagged. A chargeback carries chargebacks_status and chargeback_date instead of the refund fields.

A receiver that confirms before acting

Each example answers HTTP 200 once it has an answer from the status call, and HTTP 503 if the status call fails, so that the webhook is retried.

Examples
// Node 18 or later. No dependencies.
import http from 'node:http';

const API_KEY = '<API_KEY>';
const handled = new Set(); // keep this in your database in production

async function confirmPayin(merchantOrderId) {
  const res = await fetch('https://api.payeeglobal.com/v2/upi/status', {
    method: 'POST',
    headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({ merchant_order_id: merchantOrderId }),
  });
  return res.json();
}

http.createServer((req, res) => {
  if (req.method !== 'POST' || req.url !== '/webhooks/payin') {
    res.writeHead(404).end();
    return;
  }
  let raw = '';
  req.on('data', (chunk) => { raw += chunk; });
  req.on('end', async () => {
    let hint;
    try {
      hint = JSON.parse(raw);
    } catch {
      res.writeHead(400).end();
      return;
    }
    try {
      // Treat the body as a hint and ask the API what happened.
      const confirmed = await confirmPayin(hint.merchant_order_id);
      const key = `${confirmed.transaction_id}:${confirmed.status}`;
      const final = confirmed.status === 'success' || confirmed.status === 'failed';
      if (confirmed.merchant_order_id === hint.merchant_order_id && final && !handled.has(key)) {
        handled.add(key);
        // Fulfil or cancel the order here, using confirmed.amount rather than hint.amount.
        console.log('confirmed', confirmed.merchant_order_id, confirmed.status, confirmed.amount);
      }
      res.writeHead(200).end('ok'); // only HTTP 200 counts as delivered
    } catch {
      res.writeHead(503).end(); // any other answer is retried, up to three more times
    }
  });
}).listen(8080);

Send yourself a test webhook while you build:

Shell
# Send a sample pay-in webhook to your own endpoint while you build it.
curl -sS -X POST 'http://localhost:8080/webhooks/payin' \
  -H 'Content-Type: application/json' \
  -d '{"status":"success","merchant_order_id":"<merchant_order_id>","transaction_id":"<transaction_id>","amount":"<amount>","message":"<message>","upi":null,"utr":"<utr>"}'
Guide 06

Errors and status codes

Always read status in the response body. The HTTP status is 200 for almost every answer, including most errors. Only authentication problems use HTTP 401.

Error shapes

Pay-in, pay-in status and balance calls:

JSON
{"status": "validation_error", "message": "Phone number must be 10 digits"}

status is validation_error (HTTP 200), notfound (HTTP 200) or unauthorized (HTTP 401).

Pay-out and pay-out status calls:

JSON
{"status": 402, "message": "Insufficient Amount in your wallet."}

status is 402 (HTTP 200) or 401 (HTTP 401). When encrypted_data is missing or cannot be decrypted, a pay-out answers in the pay-in shape.

Messages you will meet

Message Call Cause What to do
Authentication failed. all no Authorization header send Authorization: Bearer <API_KEY>
Invalid secret key. all unknown key, or API access off check the key under Settings
IP address (<ip>) not whitelisted. pay-in create and status, pay-out create IP allow-list is on add the address in the console
Encrypted data must be a non-empty string. pay-in and pay-out create encrypted_data missing send {"encrypted_data": "..."}
Invalid data encryption. pay-in and pay-out create wrong key, IV, padding or encoding re-check the encryption step
Duplicate order_id, field must be unique. pay-in and pay-out create reference already used check the status of the earlier request
You are not eligible to make the payment. pay-in create membership_duration below 3 do not offer UPI to this customer yet
Invalid pg_id. pay-in create unknown terminal ID leave pg_id out unless we issued it
Phone number must be 10 digits pay-in create phone format send 10 digits, no country code
Invalid URL pay-in and pay-out create response_url or webhook_url send a full URL with a public domain
At least one of merchant_order_id, transaction_id, or utr is required. status calls empty lookup send merchant_order_id
Transaction not found. status calls no match check the reference
Payouts are only allowed through IMPS or NEFT. pay-out create payment_mode send IMPS or NEFT
Amount should be greater than or equal to 100. pay-out create below the minimum send 100 or more
Insufficient Amount in your wallet. pay-out create pay-out balance too low top up, or send less
No Payout terminal assigned. pay-out create pay-outs not set up for your account contact us

Transaction statuses

failed, success, pending, authenticate, canceled, to_be_confirm, blocked, expired, on_hold, settled. A code the API has no name for is shown as UNKNOWN.

Retrying

  • A network error or a timeout does not tell you whether the request was processed. Check the status by your own reference before sending anything again.
  • Never retry a pay-out with a new order_id.
  • A validation_error or a 402 will fail again until you change the request.
Guide 07

Testing

Test account

Testing uses small real amounts on a test merchant account that we open with you. Pay-outs have a minimum of 100.

Check your encryption offline

Before your first call, encrypt a request and decrypt it again with the same key and IV. If you get your JSON back, the encryption step is right:

Shell
printf '%s' "$ENCRYPTED" | openssl enc -d -aes-256-cbc -K "$KEY_HEX" -iv "$IV_HEX" -base64 -A

KEY_HEX and ENCRYPTED come from the cURL example in the pay-ins guide.

Test your webhook receiver locally

Run your receiver, then post a sample message to it:

Shell
# Send a sample pay-in webhook to your own endpoint while you build it.
curl -sS -X POST 'http://localhost:8080/webhooks/payin' \
  -H 'Content-Type: application/json' \
  -d '{"status":"success","merchant_order_id":"<merchant_order_id>","transaction_id":"<transaction_id>","amount":"<amount>","message":"<message>","upi":null,"utr":"<utr>"}'

Your receiver should call the status endpoint, then answer HTTP 200.

Launch checklist

  • The API key, secret hash and IV are on your servers only.
  • Every pay-in has a unique merchant_order_id; every pay-out a unique order_id.
  • Your code reads status in the body and does not rely on the HTTP status.
  • Your webhook receiver answers HTTP 200, confirms with the status call before acting, and handles duplicates.
  • After a timeout you check status before retrying, and never resend a pay-out with a new order_id.
  • response_url and webhook_url are full HTTPS URLs on a public domain.
  • If you use the IP allow-list, all of your API servers are on it.
  • You have made one small real pay-in and one pay-out of at least 100, and seen both webhooks arrive.

API reference

Pay-ins

Create a UPI pay-in

POST/v2/upi/payin

Encrypt these fields as JSON and send the result as encrypted_data. The pay-in guide has working code in five languages.

Field Required Rule
first_name, last_name Yes 2 to 100 characters
email Yes valid email
phone_number Yes 10 digits, first digit 6 to 9, no country code
amount Yes decimal INR, for example "100.00"
membership_duration Yes months as a member of your platform; below 3 is refused
response_url Yes full URL with a public domain
webhook_url Yes full URL with a public domain
merchant_order_id No your unique reference; strongly recommended
customer_user_id No your customer reference
upi No the customer's UPI ID
pg_id No terminal ID, only if we issued you several
address, city, state, zip No 2 to 250 characters
country No two capital letters

Every pay-in is in INR; currency is ignored.

Reading the answer. Most requests return status: "authenticate" with an authenticate_url and usually a upi_intent. That means the pay-in exists and is waiting for the customer. It is not paid yet. The final result arrives by webhook, and you can ask for it with the status call.

Errors come back with HTTP 200 and status: "validation_error", "blocked" or "failed". Only authentication problems use HTTP 401. Always read status in the body.

Headers

HeaderRequiredValue
AuthorizationYesBearer <API_KEY>: your API secret key. Your API secret key, sent as Authorization: Bearer <API_KEY>. It is shown under Settings in the merchant console, where you can also reset it. Keep it on your server.
Content-TypeYesapplication/json

Request fields, before encryption

Serialise these as JSON, encrypt them as described in the authentication guide, and send only the ciphertext.

Fields
{
  "merchant_order_id": "ORDER-10001",
  "first_name": "Asha",
  "last_name": "Verma",
  "email": "customer@example.com",
  "phone_number": "9000000001",
  "amount": "100.00",
  "membership_duration": 3,
  "response_url": "https://merchant.example.com/payments/return",
  "webhook_url": "https://merchant.example.com/webhooks/payin"
}

Request body sent

Body
{
  "encrypted_data": "<BASE64_CIPHERTEXT>"
}

Responses

The pay-in was created, refused or failed. Read status in the body.

{
  "status": "authenticate",
  "authenticate_url": "https://api.payeeglobal.com/<hosted-page>/<reference>",
  "upi_intent": "upi://pay?pa=<payee_vpa>&pn=<payee_name>&am=100.00&cu=INR&tr=<reference>",
  "merchant_order_id": "ORDER-10001",
  "transaction_id": "T4K2Q9ZB1790000000000",
  "amount": "100.00",
  "message": "QR Generated."
}

Example in five languages

POST /v2/upi/payin
API_KEY='<API_KEY>'
SECRET_HASH='<SECRET_HASH>'   # base64, from Settings in the merchant console
IV_HEX='<IV_HEX>'             # 32 hex characters, from the API documentation page in the console

# The AES-256 key as hex, decoded from the base64 secret hash
KEY_HEX=$(printf '%s' "$SECRET_HASH" | openssl base64 -d -A | od -An -v -tx1 | tr -d ' \n')

FIELDS='{"merchant_order_id":"<merchant_order_id>","first_name":"<first_name>","last_name":"<last_name>","email":"<email>","phone_number":"<phone_number>","amount":"<amount>","membership_duration":"<membership_duration>","response_url":"<response_url>","webhook_url":"<webhook_url>"}'

ENCRYPTED=$(printf '%s' "$FIELDS" | openssl enc -aes-256-cbc -K "$KEY_HEX" -iv "$IV_HEX" -base64 -A)

curl -sS -X POST 'https://api.payeeglobal.com/v2/upi/payin' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d "{\"encrypted_data\":\"$ENCRYPTED\"}"
Pay-ins

Get a pay-in's status

POST/v2/upi/status

Look a pay-in up by your merchant_order_id (or by utr once it has one). Plain JSON, not encrypted.

transaction_id passes validation but is not used for the lookup, so always send merchant_order_id. Use this call to confirm a webhook before you act on it.

Headers

HeaderRequiredValue
AuthorizationYesBearer <API_KEY>: your API secret key. Your API secret key, sent as Authorization: Bearer <API_KEY>. It is shown under Settings in the merchant console, where you can also reset it. Keep it on your server.
Content-TypeYesapplication/json

Request body

Plain JSON, not encrypted.

Body
{
  "merchant_order_id": "ORDER-10001"
}

Responses

The pay-in, or an error. Read status in the body.

{
  "status": "success",
  "merchant_order_id": "ORDER-10001",
  "transaction_id": "T4K2Q9ZB1790000000000",
  "amount": "100.00",
  "message": "<detail>",
  "upi": null,
  "utr": "000000000000"
}

Example in five languages

POST /v2/upi/status
curl -sS -X POST 'https://api.payeeglobal.com/v2/upi/status' \
  -H 'Authorization: Bearer <API_KEY>' \
  -H 'Content-Type: application/json' \
  -d '{"merchant_order_id":"<merchant_order_id>"}'
Pay-outs

Send a bank pay-out (IMPS or NEFT)

POST/v2/refund

The path is /v2/refund for historical reasons. It sends money from your pay-out balance to an Indian bank account.

Encrypt the fields below as JSON and send the result as encrypted_data, exactly as for a pay-in.

Field Rule
order_id your unique reference, 3 to 100 letters, digits or dashes
amount decimal INR, minimum 100
payment_mode IMPS or NEFT
bank_name, bank_account_number, ifsc_code, account_holder_name beneficiary bank details
account_type 1 savings or 2 current
customer_email valid email
customer_phone_no 10 digits, first digit 6 to 9
remark free text
response_url, webhook_url full URLs with a public domain

All fields are required. The amount plus any fee is taken from your pay-out balance as soon as the request is accepted. The usual answer is status: 303 (pending); the final result arrives by webhook.

Pay-out answers put a numeric code in status and use HTTP 200, except authentication failures and a reused order_id, which use HTTP 401. If encrypted_data is missing or cannot be decrypted, the answer uses the pay-in error shape (status: "validation_error").

Headers

HeaderRequiredValue
AuthorizationYesBearer <API_KEY>: your API secret key. Your API secret key, sent as Authorization: Bearer <API_KEY>. It is shown under Settings in the merchant console, where you can also reset it. Keep it on your server.
Content-TypeYesapplication/json

Request fields, before encryption

Serialise these as JSON, encrypt them as described in the authentication guide, and send only the ciphertext.

Fields
{
  "order_id": "PAYOUT-10001",
  "amount": "100.00",
  "payment_mode": "IMPS",
  "bank_name": "Example Bank",
  "bank_account_number": "000000000000",
  "ifsc_code": "EXMP0000001",
  "account_holder_name": "Asha Verma",
  "account_type": "1",
  "customer_email": "customer@example.com",
  "customer_phone_no": "9000000001",
  "remark": "Withdrawal",
  "response_url": "https://merchant.example.com/payouts/return",
  "webhook_url": "https://merchant.example.com/webhooks/payout"
}

Request body sent

Body
{
  "encrypted_data": "<BASE64_CIPHERTEXT>"
}

Responses

Accepted, decided or refused. Read status in the body.

{
  "status": 303,
  "message": "pending",
  "transaction": {
    "status": "pending",
    "order_id": "PAYOUT-10001",
    "transaction_id": "PX7Y2K9LM1790000000000",
    "utr": null,
    "amount": "100.00",
    "message": "Payout transaction request is in progress."
  }
}

Example in five languages

POST /v2/refund
API_KEY='<API_KEY>'
SECRET_HASH='<SECRET_HASH>'   # base64, from Settings in the merchant console
IV_HEX='<IV_HEX>'             # 32 hex characters, from the API documentation page in the console

# The AES-256 key as hex, decoded from the base64 secret hash
KEY_HEX=$(printf '%s' "$SECRET_HASH" | openssl base64 -d -A | od -An -v -tx1 | tr -d ' \n')

FIELDS='{"order_id":"<order_id>","amount":"<amount>","payment_mode":"IMPS","bank_name":"<bank_name>","bank_account_number":"<bank_account_number>","ifsc_code":"<ifsc_code>","account_holder_name":"<account_holder_name>","account_type":"<account_type>","customer_email":"<customer_email>","customer_phone_no":"<customer_phone_no>","remark":"<remark>","response_url":"<response_url>","webhook_url":"<webhook_url>"}'

ENCRYPTED=$(printf '%s' "$FIELDS" | openssl enc -aes-256-cbc -K "$KEY_HEX" -iv "$IV_HEX" -base64 -A)

curl -sS -X POST 'https://api.payeeglobal.com/v2/refund' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d "{\"encrypted_data\":\"$ENCRYPTED\"}"
Pay-outs

Get a pay-out's status

POST/v2/refund/status

Look a pay-out up by transaction_id, by your order reference (sent as merchant_order_id) or by utr. Plain JSON, not encrypted.

Headers

HeaderRequiredValue
AuthorizationYesBearer <API_KEY>: your API secret key. Your API secret key, sent as Authorization: Bearer <API_KEY>. It is shown under Settings in the merchant console, where you can also reset it. Keep it on your server.
Content-TypeYesapplication/json

Request body

Plain JSON, not encrypted.

Body
{
  "merchant_order_id": "PAYOUT-10001"
}

Responses

The pay-out, or an error. Read status in the body.

{
  "status": 200,
  "message": "success",
  "transaction": {
    "status": "success",
    "order_id": "PAYOUT-10001",
    "transaction_id": "PX7Y2K9LM1790000000000",
    "utr": "000000000000",
    "amount": "100.00",
    "message": "<detail>"
  }
}

Example in five languages

POST /v2/refund/status
curl -sS -X POST 'https://api.payeeglobal.com/v2/refund/status' \
  -H 'Authorization: Bearer <API_KEY>' \
  -H 'Content-Type: application/json' \
  -d '{"merchant_order_id":"<order_id>"}'
Balance

Get balances and totals

POST/v2/transaction/balance

Your pay-in, settlement, reserve and pay-out balances, with pay-in totals. No request body.

Headers

HeaderRequiredValue
AuthorizationYesBearer <API_KEY>: your API secret key. Your API secret key, sent as Authorization: Bearer <API_KEY>. It is shown under Settings in the merchant console, where you can also reset it. Keep it on your server.

Request body

No request body.

Responses

Balances for an account with no pay-ins yet. With data, the counts, amounts and rates are decimal strings.

{
  "status": "success",
  "unsettle_amount": "0.00",
  "available_balance": "0.00",
  "rolling_reserve": 0,
  "payout_balance": "0.00",
  "succeeded_payout": 0,
  "success_transactions": 0,
  "success_amount": 0,
  "success_rate": 0,
  "failed_transactions": 0,
  "failed_amount": 0,
  "failure_rate": 0
}

Example in five languages

POST /v2/transaction/balance
curl -sS -X POST 'https://api.payeeglobal.com/v2/transaction/balance' \
  -H 'Authorization: Bearer <API_KEY>' \
  -H 'Content-Type: application/json'

Webhook events

The three messages we post to your webhook_url. Confirm each with the status call before you act on it. The webhooks guide has receivers in four languages.

Webhook event

Pay-in reached a final or held state

POSTyour webhook_url

Posted as JSON (Content-Type: application/json) to the webhook_url you sent. Respond with HTTP 200: any other code, including 201 and 204, counts as a failed delivery. Delivery times out after 60 seconds.

Treat a webhook as a prompt to check, never as proof: before you credit or pay anything, confirm the outcome with the status call.

Sent when a pay-in becomes success, failed, blocked or on_hold. There is no webhook for canceled or expired. A failed delivery is retried once a minute for pay-ins at least five minutes old, up to three more times (four attempts in all). The same status can arrive more than once: make your handler idempotent on transaction_id and status.

Payload

Payload
{
  "status": "success",
  "merchant_order_id": "ORDER-10001",
  "transaction_id": "T4K2Q9ZB1790000000000",
  "amount": "100.00",
  "message": "<detail>",
  "upi": null,
  "utr": "000000000000"
}

Respond with HTTP 200 to acknowledge.

Webhook event

Pay-out succeeded or failed

POSTyour webhook_url

Posted as JSON (Content-Type: application/json) to the webhook_url you sent. Respond with HTTP 200: any other code, including 201 and 204, counts as a failed delivery. Delivery times out after 60 seconds.

Treat a webhook as a prompt to check, never as proof: before you credit or pay anything, confirm the outcome with the status call.

Sent when a pay-out becomes success or failed. Note the reference key is order_id here, not merchant_order_id. A failed delivery is retried from 15 minutes after the status change, up to three more times.

Payload

Payload
{
  "status": "success",
  "order_id": "PAYOUT-10001",
  "transaction_id": "PX7Y2K9LM1790000000000",
  "utr": "000000000000",
  "amount": "100.00",
  "message": "<detail>"
}

Respond with HTTP 200 to acknowledge.

Webhook event

Refund, chargeback or flag recorded on a pay-in

POSTyour webhook_url

Sent when our operations team records a refund, a chargeback or a flag against one of your pay-ins. Posted as JSON to the pay-in's webhook_url (or your account webhook URL). Sent once and not retried. The console's transaction pages show the same information; check there before you act on one.

Payload

Payload
{
  "status": 200,
  "event": "refund",
  "message": "Transaction refund successfully.",
  "transaction": {
    "merchant_order_id": "ORDER-10001",
    "transaction_id": "T4K2Q9ZB1790000000000",
    "amount": "100.00",
    "message": "<detail>",
    "utr": "000000000000",
    "refund_status": true,
    "refund_date": "<date>"
  }
}

Respond with HTTP 200 to acknowledge.

Reference

Errors

Always read status in the body. The HTTP status is 200 for almost every answer, including most errors; only authentication problems use HTTP 401.

Shapestatus valuesHTTP
Error (pay-in, status and balance calls)validation_error, unauthorized, notfoundReturned with HTTP 200, except unauthorized, which is HTTP 401.
Error (pay-out calls)401, 402status 402 is returned with HTTP 200; status 401 with HTTP 401.
Pay-in, status and balance calls
{
  "status": "validation_error",
  "message": "Phone number must be 10 digits"
}
Pay-out calls
{
  "status": 402,
  "message": "Insufficient Amount in your wallet."
}

Transaction statuses

failed, success, pending, authenticate, canceled, to_be_confirm, blocked, expired, on_hold, settled, UNKNOWN

authenticate: waiting for the customer to pay. success and failed are final. canceled: the customer cancelled, or the hosted page timed out. expired: left unpaid for two days. blocked: refused before it reached a rail. on_hold: held for review. settled: included in a settlement. to_be_confirm and pending are rare intermediate states. UNKNOWN: a code the API has no name for.

Every message, its cause and what to do is in the errors guide.

Before you integrate

Ask us for a test merchant account at support@payeeglobal.com. Keys, the secret hash and the IV are issued in the merchant console; sign in to the merchant console.