# NiriPay Developer Documentation

Source-reviewed 27 October 2026. Examples are illustrative.

# Build with NiriPay

Connect your application to NiriPay with server-side APIs, hosted checkout, and signed payment notifications. Start with an INR payment, or explore the separate USDT integration.

[Start building](/quickstart/) · [Explore the API](/create-deposit/)

## Make your first request

Create an INR deposit and use the returned `payment_page` to open checkout. The example below uses illustrative values; requests sent to the production API create real orders.

```bash
curl -X POST https://api.niripay.com/api/v1/merchant/deposits/create \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 1000,
    "transaction_id": "ORDER-2026-001",
    "webhook_url": "https://merchant.example/webhooks/niripay"
  }'
```

## A payment, from end to end

1. Create an order on your server.
2. Open hosted checkout for your customer.
3. Verify the notification and reconcile payment status.

Your server creates the payment. Your customer completes checkout. Your application verifies the notification and reconciles the final status.

> A successful API request is not a successful payment. Use the transaction’s payment status before fulfilling an order.

## Choose your integration

| Integration | What you can build | Start here |
| --- | --- | --- |
| INR collections | UPI payments and hosted checkout | [Create a deposit](/create-deposit/) |
| INR payouts | Bank transfers and status tracking | [Create a payout](/create-payout/) |
| USDT on TRON | Reusable deposit addresses and quoted payouts | [USDT overview](/crypto-overview/) |
| WooCommerce | Checkout integration for your store | [WooCommerce guide](/woocommerce/) |

## Before you begin

Have an approved merchant account, a server-side API key, an allowlisted server egress IP, and an HTTPS webhook endpoint ready. USDT functionality requires separate merchant activation.

Read the [quickstart](/quickstart/) for the complete flow, then follow the [production checklist](/production-checklist/) before accepting payments.

---

# Your first integration

## 1. Prepare your account

Retrieve your API key from the [merchant dashboard](https://dashboard.niripay.com). Add your backend server’s public egress IP to **API Key & IP Whitelist**. Your merchant account must be approved.

Keep the key on your server. Never include it in frontend bundles or mobile applications.

## 2. Create a payment

Send a unique merchant `transaction_id`, an INR `amount`, and your `webhook_url` to the [create deposit endpoint](/create-deposit/). Persist your request and the returned identifiers before redirecting your customer.

Use the interactive request example generator on the website, or copy the [create deposit examples](/create-deposit/). No request is sent by the generator.

## 3. Open hosted checkout

Redirect the customer to `data.payment_page` from the create response. Checkout handles the available payment options and any required evidence submission.

A return to your `return_url` is navigation, not proof that a payment succeeded.

## 4. Verify the notification

Receive the webhook on your server. Verify its HMAC-SHA256 signature against the exact raw body, check the timestamp, and store the event idempotently. See [webhook verification](/webhooks/) for a complete Node.js example.

## 5. Reconcile the payment

Query [deposit status](/deposit-status/) with your original transaction ID. Only fulfill the order when its payment status is `success`. Apply each financial effect once, including when a callback repeats.

```js
const response = await fetch(
  "https://api.niripay.com/api/v1/merchant/deposits/status",
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.NIRIPAY_API_KEY}`,
      "Content-Type": "application/json"
    },
    body: JSON.stringify({ transaction_id: "ORDER-2026-001" })
  }
);
const result = await response.json();
if (!response.ok || result.status !== "success") {
  throw new Error(result.message || "Status lookup failed");
}
// An accepted status lookup may still describe a pending payment.
if (result.data.status === "success") {
  // Persist payment and fulfillment idempotently in your database.
}
```

## If a response is delayed

Keep the original transaction ID and check its status. Do not blindly create a replacement payment or payout after a timeout. Use capped backoff for polling and escalate unresolved records with sanitized details.

---

# Authentication

## Base URL

```text
https://api.niripay.com
```

## Authentication

All APIs in this document use the same merchant authentication:

```http
Authorization: Bearer YOUR_API_KEY
```

Keep the API key on your server. Do not expose it in browser code, mobile applications, screenshots, logs, or public repositories.

## IP whitelist

Add every production server egress IP to the merchant dashboard under **API Key & IP Whitelist**. Requests from an IP that is not allowed are rejected even when the API key is valid.

## Request formats

- JSON APIs use `Content-Type: application/json`.
- File upload APIs use `multipart/form-data`.
- When using `FormData` or a multipart HTTP library, let the client generate the multipart boundary. Do not set the `Content-Type` boundary manually.
- Always inspect the response body's `status` field. Some business and validation errors use HTTP `200` with `"status": "error"`.

## Common response envelope

Success:

```json
{
  "status": "success",
  "message": "Human-readable result",
  "data": {}
}
```

Error:

```json
{
  "status": "error",
  "message": "Description of the problem"
}
```



## Server-side request helper

The JavaScript snippets in the API reference use this helper. Configure `NIRIPAY_API_KEY` in your server environment.



```js
const BASE_URL = "https://api.niripay.com";
const API_KEY = process.env.NIRIPAY_API_KEY;

async function niripayJson(path, body) {
  const response = await fetch(`${BASE_URL}${path}`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${API_KEY}`,
      "Content-Type": "application/json"
    },
    body: JSON.stringify(body)
  });

  const result = await response.json();

  if (!response.ok || result.status !== "success") {
    throw new Error(result.message || `NiriPay request failed (${response.status})`);
  }

  return result;
}
```

---

# Payment lifecycle

## Two statuses, two different questions

The top-level `status` tells you whether the API request succeeded. The transaction’s `data.status` tells you whether the payment is pending, successful, or failed. Manual verification is a separate workflow.

| Value | Meaning |
| --- | --- |
| `status: "success"` | The API request was accepted or processed successfully |
| `data.status: "pending"` | Payment has not reached a final successful state |
| `data.status: "success"` | The transaction is recorded as successful |
| `manual_verification_status` | The current evidence review state; not proof of funds |

## Hosted checkout flow

1. Create an order on your server.
2. Open hosted checkout for your customer.
3. Verify the notification and reconcile payment status.

After the customer pays, checkout may request a screenshot, a UTR, or additional documents. Browser OCR helps read details; it cannot confirm receipt of money.

## Evidence matching

The evidence workflow compares UTRs and recipient UPI details with eligible orders. Missing, conflicting, masked, or ambiguous details can require manual review. A screenshot can match an order without the payment being confirmed.

Use the [v2 evidence endpoint](/payment-evidence/) for its documented evidence contract. The [v1 screenshot endpoint](/submit-screenshot/) remains a distinct compatibility flow with different fields and responses.

## Complete the order once

Verify callbacks, query status where needed, and enforce idempotency in your own ledger and fulfillment logic. A duplicate webhook or a repeated status check must not credit a customer twice.

## Handle missing notifications

Use the [recovery guide](/recovery/) to reconcile records when your server is unavailable or delivery is delayed.

---

# Create a deposit

Creates an INR deposit and returns the generated payment instructions.

## Request fields

| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `webhook_url` | URL | Yes | Valid public URL | Receives deposit status postbacks |
| `return_url` | URL | No | Valid URL | Customer redirect after payment |
| `amount` | Number | Yes | INR 100 to 50,000 | Deposit amount |
| `transaction_id` | String | Yes | Non-empty and unique | Your merchant transaction ID |
| `customer_id` | String | No | Non-empty if provided | Your customer identifier |
| `customer_name` | String | No | Non-empty after trimming | Customer name forwarded to supported providers |
| `customer_email` | String | No | Valid email | Customer email forwarded to supported providers |
| `customer_mobile` | String | No | Exactly 10 digits | Customer mobile number |
| `traffic_type` | String | No | `ftd` or `trusted` | Optional traffic classification |

## cURL example

```bash
curl -X POST https://api.niripay.com/api/v1/merchant/deposits/create \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "webhook_url": "https://merchant.example/webhooks/deposit",
    "return_url": "https://merchant.example/payment-complete",
    "amount": 1000,
    "transaction_id": "TXN123456789",
    "customer_id": "CUSTOMER-101"
  }'
```

## JavaScript example

```js
const deposit = await niripayJson("/api/v1/merchant/deposits/create", {
  webhook_url: "https://merchant.example/webhooks/deposit",
  return_url: "https://merchant.example/payment-complete",
  amount: 1000,
  transaction_id: "TXN123456789",
  customer_id: "CUSTOMER-101"
});
```

## Success response

```json
{
  "status": "success",
  "message": "Deposit created successfully",
  "data": {
    "payment_page": "https://pay.niripay.com/pay/abc123def456ghi789",
    "intent_url": "upi://pay?pa=merchant@upi&am=1000&cu=INR",
    "is_native": false,
    "transaction_id": "TXN123456789",
    "internal_transaction_id": "abc123def456ghi789",
    "amount": 1000,
    "fees": {
      "percentage": 3,
      "fixed_amount": 2,
      "total_amount": 32.0,
      "net_credit_amount": 968.0
    }
  }
}
```

## Important rules

- Each create request must use a new `transaction_id`.
- The minimum amount is INR 100 and the maximum is INR 50,000.
- Persist both `transaction_id` and `internal_transaction_id`.
- Store the create response before redirecting the customer.

---

# Get deposit status

Returns the current payment and manual-verification state of a deposit owned by the authenticated merchant.

## Request fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `transaction_id` | String | Yes | Merchant transaction ID used to create the deposit |

## cURL example

```bash
curl -X POST https://api.niripay.com/api/v1/merchant/deposits/status \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"transaction_id":"TXN123456789"}'
```

## JavaScript example

```js
const depositStatus = await niripayJson("/api/v1/merchant/deposits/status", {
  transaction_id: "TXN123456789"
});
```

## Example response

```json
{
  "status": "success",
  "message": "Deposit status fetched successfully",
  "data": {
    "transaction_id": "TXN123456789",
    "internal_transaction_id": "abc123internal456",
    "intent_url": "upi://pay?pa=merchant@upi&am=1000&cu=INR",
    "amount": 1000,
    "status": "pending",
    "manual_verification_status": "need_pdf_video",
    "fees": {
      "percentage": 3,
      "fixed_amount": 2,
      "total_amount": 32.0,
      "net_credit_amount": 968.0
    }
  }
}
```

## Manual verification states

| State | Meaning or next action |
| --- | --- |
| `need_screenshot` | Upload the payment screenshot |
| `screenshot_submitted` | Screenshot was received |
| `checking_by_gateway` | UTR or evidence is being checked |
| `need_pdf_video` | Upload both PDF and video |
| `pdf_video_submitted` | PDF and video were received for review |
| `invalid_screenshot` | Screenshot could not be accepted |
| `rejected` | Manual verification was rejected |
| `wrong_order` | Evidence does not match the order |
| `not_received` | Payment was not received |
| `duplicate` | Evidence or payment appears duplicated |
| `duplicate_success` | A matching payment was already completed successfully |

Use the status API after receiving a webhook to reconcile your local state, and as a fallback when a webhook does not arrive.

---

# Submit a UTR

Submits the payment UTR for a pending deposit. This endpoint uses the same merchant API key and merchant ownership rules as Create Deposit.

## Request fields

| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `transaction_id` | String | Yes | Non-empty | Merchant transaction ID used to create the deposit |
| `utr` | String | Yes | Exactly 12 characters | Payment UTR or reference number |
| `upi_id` | String | No | Must match the deposit payment slot | Omit it to let NiriPay select the deposit slot automatically |

## cURL example

```bash
curl -X POST https://api.niripay.com/api/v1/merchant/deposits/submit-utr \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "transaction_id": "TXN123456789",
    "utr": "123456789012"
  }'
```

## JavaScript example

```js
const utrResult = await niripayJson("/api/v1/merchant/deposits/submit-utr", {
  transaction_id: "TXN123456789",
  utr: "123456789012"
});
```

## Success response

```json
{
  "status": "success",
  "message": "UTR submitted successfully",
  "data": {
    "transaction_id": "TXN123456789",
    "internal_transaction_id": "abc123internal456",
    "submission_type": "utr",
    "manual_verification_status": "checking_by_gateway"
  }
}
```

## Submission rules

- The deposit must belong to the authenticated merchant.
- Evidence is accepted only while the deposit is pending.
- Do not submit the same UTR twice.
- If `upi_id` is provided, it must belong to this deposit's payment slot.

---

# Payment evidence v2

Accepts merchant-owned payment evidence. This endpoint is separate from the v1 screenshot submission API.

## Multipart fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `transaction_id` | String | Yes | Your merchant transaction ID |
| `utr` | String | Yes | Exactly 12 digits |
| `screenshots` | Image file | Yes | One or two images; maximum 10 MB per file |
| `upi_id` | String | No | Customer-supplied recipient UPI ID |
| `frontend_ocr` | JSON string | No | Array of `{ "utr": string, "upi_ids": string[] }` hints; backend OCR remains authoritative for evidence matching |

## cURL example

```bash
curl -X POST https://api.niripay.com/api/v2/merchant/deposits/evidence \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -F "transaction_id=ORDER-2026-001" \
  -F "utr=123456789012" \
  -F "screenshots=@payment-receipt.png"
```

## JavaScript example

```js
// Server-side JavaScript. screenshotFile is a Blob or File read on your server.
const form = new FormData();
form.append("transaction_id", "ORDER-2026-001");
form.append("utr", "123456789012");
form.append("screenshots", screenshotFile, "payment-receipt.png");
const response = await fetch(
  "https://api.niripay.com/api/v2/merchant/deposits/evidence",
  { method: "POST", headers: { Authorization: `Bearer ${process.env.NIRIPAY_API_KEY}` }, body: form }
);
const result = await response.json();
if (!response.ok || result.status !== "success") {
  throw new Error(result.message || "Evidence submission failed");
}
```

## Accepted response

HTTP `201` indicates acceptance. These example IDs are illustrative. Read the actual returned `code` and `ocr_state`; processing can still be in progress.

```json
{
  "status": "success",
  "message": "Payment evidence accepted",
  "data": {
    "evidence_id": "EXAMPLE_EVIDENCE_ID",
    "code": "ANALYZING",
    "message": {
      "en": "Screenshot accepted. We are checking the payment details.",
      "hi": "स्क्रीनशॉट मिल गया है। हम भुगतान विवरण जाँच रहे हैं।"
    },
    "requested_transaction_id": "EXAMPLE_INTERNAL_TRANSACTION_ID",
    "matched_transaction_id": null,
    "ocr_state": "pending"
  }
}
```

Response transaction identifiers describe resolved internal order references. Retain your original merchant transaction ID for the merchant status API.

## Evidence outcomes

| Code | Meaning |
| --- | --- |
| `ANALYZING` | Screenshot processing is pending |
| `MATCHED_CURRENT_ORDER` | Evidence matches the current order |
| `MATCHED_OTHER_ORDER` | Evidence matches another eligible recent order |
| `MASKED_RECIPIENT` | Recipient information is hidden; review is needed |
| `MISSING_UTR` / `MISSING_RECIPIENT` | Required details could not be read |
| `OCR_CONFLICT` / `AMBIGUOUS_ORDER` | Details conflict or more than one order may match |
| `UPI_NOT_OURS` | Recipient does not match eligible payment instructions |
| `DUPLICATE_PENDING` / `DUPLICATE_SUCCESS` | The transaction reference is already associated with a payment |
| `MANUAL_REVIEW` | Evidence needs operator review |
| `VERIFIED` | Screenshot UTR and recipient match; not a final payment success status |
| `NATIVE_SCREENSHOT_UNSUPPORTED` | This flow does not apply to a native payment order |

## Confirm the payment separately

Never credit an order on evidence acceptance or OCR verification alone. Query [deposit status](/deposit-status/) with the original merchant transaction ID and reconcile the final payment status.

Validation failures return HTTP `400`; classified conflicts may return HTTP `409` with details under `data.code` and `data.message`.

---

# Submit a screenshot

> This is the v1 screenshot flow. The [v2 evidence API](/payment-evidence/) uses a different file field and response contract.

Uploads an image of the payment receipt and records it with the UTR as a single evidence submission.

## Multipart fields

| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `transaction_id` | String | Yes | Non-empty | Merchant transaction ID used to create the deposit |
| `utr` | String | Yes | Exactly 12 characters | UTR shown in the screenshot |
| `upi_id` | String | No | Must match the deposit payment slot | Omit it for automatic slot selection |
| `screenshot` | Image file | Yes | `image/*`, maximum 10 MB | Payment receipt screenshot |

## cURL example

```bash
curl -X POST https://api.niripay.com/api/v1/merchant/deposits/submit-screenshot \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -F "transaction_id=TXN123456789" \
  -F "utr=123456789012" \
  -F "screenshot=@payment-receipt.png"
```

## JavaScript example

```js
const form = new FormData();
form.append("transaction_id", "TXN123456789");
form.append("utr", "123456789012");
form.append("screenshot", screenshotFile);

const response = await fetch(
  "https://api.niripay.com/api/v1/merchant/deposits/submit-screenshot",
  {
    method: "POST",
    headers: { Authorization: `Bearer ${API_KEY}` },
    body: form
  }
);

const screenshotResult = await response.json();

if (!response.ok || screenshotResult.status !== "success") {
  throw new Error(screenshotResult.message || "Screenshot submission failed");
}
```

## Success response

```json
{
  "status": "success",
  "message": "Screenshot submitted successfully",
  "data": {
    "transaction_id": "TXN123456789",
    "internal_transaction_id": "abc123internal456",
    "submission_type": "screenshot",
    "manual_verification_status": "need_pdf_video"
  }
}
```

An accepted screenshot changes `manual_verification_status` to `need_pdf_video`. Submit the PDF and video through the next endpoint.

---

# Submit PDF & video

Uploads a bank-statement PDF and verification video for a deposit whose `manual_verification_status` is `need_pdf_video`. Both files must be submitted in the same request.

## Multipart fields

| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `transaction_id` | String | Yes | Merchant or NiriPay internal ID | The merchant transaction ID is recommended |
| `pdf` | PDF file | Yes | `application/pdf`, maximum 250 MB | Bank statement or payment proof |
| `video` | Video file | Yes | `video/*`, maximum 250 MB | Verification video |
| `pdf_password` | String | No | Required when PDF is encrypted | Password used to open the PDF |

## cURL example

```bash
curl -X POST https://api.niripay.com/api/v1/merchant/deposits/submit-pdf-video \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -F "transaction_id=TXN123456789" \
  -F "pdf=@bank-statement.pdf;type=application/pdf" \
  -F "video=@verification.mp4;type=video/mp4" \
  -F "pdf_password=OPTIONAL_PASSWORD"
```

## JavaScript example

```js
const form = new FormData();
form.append("transaction_id", "TXN123456789");
form.append("pdf", pdfFile);
form.append("video", videoFile);

if (pdfPassword) {
  form.append("pdf_password", pdfPassword);
}

const response = await fetch(
  "https://api.niripay.com/api/v1/merchant/deposits/submit-pdf-video",
  {
    method: "POST",
    headers: { Authorization: `Bearer ${API_KEY}` },
    body: form
  }
);

const documentsResult = await response.json();

if (!response.ok || documentsResult.status !== "success") {
  throw new Error(documentsResult.message || "Document submission failed");
}
```

## Success response

```json
{
  "status": "success",
  "message": "Documents submitted successfully",
  "data": {
    "pdf_url": "uploads/documents/TXN123456789_statement.pdf",
    "video_url": "uploads/documents/TXN123456789_verification.mp4"
  }
}
```

After acceptance, the deposit's `manual_verification_status` changes to `pdf_video_submitted`.

---

# Create a payout

Creates an INR bank withdrawal.

## Request fields

| Field | Type | Required | Constraints | Description |
| --- | --- | --- | --- | --- |
| `webhook_url` | URL | Yes | Valid public URL | Receives withdrawal status postbacks |
| `amount` | Integer | Yes | INR 100 to 100,000 | Withdrawal amount; decimals are not accepted |
| `transaction_id` | String | Yes | Unique, 7 to 32 characters | Your merchant withdrawal ID |
| `beneficiary_name` | String | Yes | Non-empty | Bank account beneficiary |
| `bank_name` | String | Yes | Non-empty | Beneficiary bank name |
| `bank_account` | String | Yes | Non-empty | Beneficiary account number |
| `ifsc` | String | Yes | Non-empty | Beneficiary bank branch IFSC |

## cURL example

```bash
curl -X POST https://api.niripay.com/api/v1/merchant/withdrawals/create \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "webhook_url": "https://merchant.example/webhooks/withdrawal",
    "amount": 5000,
    "transaction_id": "WD-2026-001",
    "beneficiary_name": "Test Merchant",
    "bank_name": "Example Bank",
    "bank_account": "1234567890123",
    "ifsc": "EXAM0001234"
  }'
```

## JavaScript example

```js
const withdrawal = await niripayJson("/api/v1/merchant/withdrawals/create", {
  webhook_url: "https://merchant.example/webhooks/withdrawal",
  amount: 5000,
  transaction_id: "WD-2026-001",
  beneficiary_name: "Test Merchant",
  bank_name: "Example Bank",
  bank_account: "1234567890123",
  ifsc: "EXAM0001234"
});
```

## Success response

```json
{
  "status": "success",
  "message": "Withdrawal created successfully",
  "data": {
    "transaction_id": "WD-2026-001",
    "internal_transaction_id": "example-internal-id",
    "amount": 5000,
    "status": "pending",
    "fees": {
      "percentage": 3,
      "fixed_amount": 2,
      "total_amount": 152,
      "total_debit_amount": 5152
    }
  }
}
```

## Important rules

- Amount must be a whole number from INR 100 to INR 100,000.
- `transaction_id` must be unique for the merchant and 7 to 32 characters long.
- Persist the API response before considering the request accepted.
- Do not retry an accepted create request with the same transaction ID.

---

# Get payout status

Returns the current state of a withdrawal owned by the authenticated merchant.

## Request fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `transaction_id` | String | Yes | Merchant withdrawal transaction ID |

## cURL example

```bash
curl -X POST https://api.niripay.com/api/v1/merchant/withdrawals/status \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"transaction_id":"WD-2026-001"}'
```

## JavaScript example

```js
const withdrawalStatus = await niripayJson("/api/v1/merchant/withdrawals/status", {
  transaction_id: "WD-2026-001"
});
```

## Success response

```json
{
  "status": "success",
  "data": {
    "transaction_id": "WD-2026-001",
    "amount": 5000,
    "status": "success",
    "proof": "UTR1234567890123456",
    "fees": {
      "percentage": 3,
      "fixed_amount": 2,
      "total_amount": 152.0,
      "total_debit_amount": 5152.0
    }
  }
}
```

All successful status responses include `fees` (an object or null). Other response fields depend on the withdrawal state:

- `pending`: includes `transaction_id`, `amount`, and `status`.
- `success`: also includes `proof`, which contains the bank UTR.
- `failed`: the amount is reversed and credited back to the merchant wallet.

---

# USDT overview

## A separate payment contract

USDT payments use the `/api/v1/merchant/crypto` API family and require merchant activation. Your existing INR endpoints and amount rules remain unchanged.

Authenticate with the same server-side merchant Bearer API key. Crypto amounts are decimal strings with no more than six fractional digits. Do not calculate USDT using floating-point arithmetic.

```json
{
  "amount": "200.000000",
  "address": "YOUR_VALID_TRON_RECIPIENT_ADDRESS"
}
```

The address above is a placeholder. Replace it with a validated TRON address in your integration.

## Receive payments

Create reusable customer addresses with a stable `customer_id`. Each received transfer has a distinct receipt ID, even when one customer sends the same amount twice.

Credit is based on the actual received amount and its recorded fee. A customer-supplied transaction hash is not proof of payment.

[Create customer addresses →](/crypto-addresses/)

## Understand availability

| State | Meaning |
| --- | --- |
| `pending_confirmation` | Awaiting the required confirmation process |
| `pending_collection` | Awaiting collection before funds become available |
| `available` | Funds are available under the wallet’s current restrictions |
| `frozen` | Funds are not spendable |
| `dust` / `orphaned` | Receipt is not spendable |

## Send payouts

Request a quote, review its amount and fee, then submit it with a stable idempotency key. One eligible wallet must cover the payout amount and fee; separate wallets cannot be combined to fund it.

[Create a USDT payout →](/crypto-payouts/)

## Receive notifications

Crypto callbacks contain `event_id`, `event`, and `transaction`. Verify signatures using the [shared HMAC contract](/webhooks/) and deduplicate by `event_id`. Deliveries can repeat; each attempt receives a fresh signature and timestamp.

When events arrive out of order or the outcome is unclear, query the original transaction status.

---

# Customer addresses

## Request fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `customer_id` | String | Yes | Stable identifier, 1–128 characters |
| `webhook_url` | String | No | Your callback URL; keep it stable for this customer |

No invoice amount or order ID is required. Repeating the same customer ID under the same merchant returns the saved addresses and payment link. Changing its saved callback URL can produce an `IDEMPOTENCY_CONFLICT`.

## cURL example

```bash
curl -X POST https://api.niripay.com/api/v1/merchant/crypto/addresses \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "customer_id": "customer-42",
    "webhook_url": "https://merchant.example/webhooks/crypto"
  }'
```

## JavaScript example

```js
const instructions = await niripayJson("/api/v1/merchant/crypto/addresses", {
  customer_id: "customer-42",
  webhook_url: "https://merchant.example/webhooks/crypto"
});
```

## Response fields

The standard `status` / `data` envelope contains these instruction fields:

| Field | Meaning |
| --- | --- |
| `currency` / `network` | `USDT` / `TRON` |
| `payment_type` | `static_wallet` |
| `payment_link` | Reusable hosted payment instructions |
| `addresses` | Saved address or null, keyed by provider |
| `enabled` / `providers` | Merchant activation and provider availability |
| `ready` | Crypto is enabled and at least one enabled wallet has an address |
| `customer_id` / `purpose` | Customer association and address purpose |
| `provisioning_error` | Provisioning issue, or null |
| `cutoff` / `fee_bps` | Configured fee-policy values; receipt pricing remains authoritative |

## Handle partial provisioning

If an address is missing, retry this authenticated request with the same customer and callback details. Refreshing the checkout page only reads saved instructions; it does not provision missing addresses. Payments to saved addresses continue to be recognized.

Use the exact API identifiers when integrating. Dashboard presentation labels are not replacements for provider keys or transaction IDs.

---

# Balances & funding

## Read balances

```bash
curl https://api.niripay.com/api/v1/merchant/crypto/balances \
  -H "Authorization: Bearer YOUR_API_KEY"
```

The response has a `status` / `data` envelope. `data` includes `currency`, `network`, `enabled`, and a `providers` map.

| Provider balance field | Meaning |
| --- | --- |
| `available` | Spendable balance; zero when the provider is blocked |
| `reserved` | Funds reserved for operations |
| `pending_confirmation` | Receipts awaiting confirmation |
| `pending_collection` | Receipts awaiting collection |
| `frozen` | Non-spendable net total: frozen receipts plus blocked available funds |
| `frozen_receipts` | Expected net value of frozen receipts |
| `frozen_receipts_gross` | Gross value of frozen receipts |
| `blocked_available` | Available funds restricted by a provider block |
| `blocked` | Whether the provider is blocked |

The frozen breakdown fields are components, not additional balances to sum. Operating capital and internal collections are excluded from merchant-facing history.

## Retrieve top-up instructions

```bash
curl -X POST https://api.niripay.com/api/v1/merchant/crypto/topup \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
```

This returns reusable USDT top-up instructions in the same instruction structure as [customer addresses](/crypto-addresses/), with top-up purpose and no customer association. Creating instructions does not transfer funds.

## Fund one wallet for a payout

One enabled wallet must cover the amount and fee. Balances cannot be combined across wallets. INR balances cannot fund USDT payouts.

This API is separate from the admin-approved [USDT-to-INR top-up workflow](/settlements/).

---

# USDT payouts

## 1. Request a quote

`POST /api/v1/merchant/crypto/payouts/quote`

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `amount` | Decimal string | Yes | Positive recipient amount, up to six decimal places |
| `address` | String | Yes | Valid TRON recipient address |

```bash
curl -X POST https://api.niripay.com/api/v1/merchant/crypto/payouts/quote \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"amount":"200.000000","address":"YOUR_VALID_TRON_RECIPIENT_ADDRESS"}'
```

Replace the placeholder address before using the API. A quote response contains `quote_id`, `provider`, `amount`, `charge`, `total`, `destination`, `expires_at`, `currency`, and `network` under `data`.

The recipient receives the quoted `amount`; the `charge` is additional. Review the quote before submitting it.

## 2. Submit the quote

`POST /api/v1/merchant/crypto/payouts`

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `quote_id` | String | Yes | Exact ID from the accepted quote |
| `idempotency_key` | String | Yes | 8–128 letters, digits, dots, underscores, or hyphens |
| `webhook_url` | String | No | Stable callback URL for this payout |

```bash
curl -X POST https://api.niripay.com/api/v1/merchant/crypto/payouts \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "quote_id": "QUOTE_ID_FROM_RESPONSE",
    "idempotency_key": "payout-order-123",
    "webhook_url": "https://merchant.example/webhooks/crypto"
  }'
```

## 3. Track the original payout

Persist the returned transaction `id` and inspect its `state`. Query [transaction status](/crypto-transactions/) or process verified crypto callbacks. Request acceptance does not mean the blockchain transfer has completed.

## Retry rules

Reuse the same idempotency key, quote ID, and callback URL for retries of the same payout. A different request under the same key returns a conflict.

If the outcome is unknown, query the original payout; do not create a replacement. Expired or changed quotes need a fresh quote and review before a new, unsubmitted payout is authorized.

## Common errors

| Code | Next action |
| --- | --- |
| `FRAGMENTED_OR_INSUFFICIENT_LIQUIDITY` | Fund one eligible wallet or resolve pending collections |
| `INVALID_KEY` | Use a stable key with the documented format |
| `IDEMPOTENCY_CONFLICT` | Restore the original request values |
| `QUOTE_EXPIRED` / `QUOTE_CHANGED` | Review a fresh quote before initiating a new payout |
| `NOT_FOUND` | Check the quote ID and merchant ownership |

---

# Transactions & events

## List transactions

```bash
curl https://api.niripay.com/api/v1/merchant/crypto/transactions \
  -H "Authorization: Bearer YOUR_API_KEY"
```

Returns merchant receipts and payouts in `data`. The current history implementation returns at most 1,000 records; it is not a paginated archival export.

## Get transaction status

`GET /api/v1/merchant/crypto/transactions/{encoded-id}`

```js
const transactionId = "EXACT_ID_FROM_API_RESPONSE";
const response = await fetch(
  `https://api.niripay.com/api/v1/merchant/crypto/transactions/${encodeURIComponent(transactionId)}`,
  { headers: { Authorization: `Bearer ${process.env.NIRIPAY_API_KEY}` } }
);
const result = await response.json();
if (!response.ok || result.status !== "success") {
  throw new Error(result.message || "Transaction lookup failed");
}
```

Use the exact API ID, URL-encoded in the path. Do not substitute dashboard display references.

## Transaction fields

Fields depend on whether the transaction is a receipt or payout.

| Field | Meaning |
| --- | --- |
| `id` / `type` | Transaction identity and kind |
| `purpose` / `provider` | Address purpose and provider key |
| `customer_id` | Associated customer, when present |
| `address` / `txid` | Address and blockchain transaction reference, when available |
| `transfer_index` | Transfer index, where provided |
| `gross` / `fee` / `net` | Receipt amounts as decimal strings |
| `amount` / `charge` | Payout recipient amount and additional charge |
| `state` | Current transaction state |
| `currency` / `network` | `USDT` / `TRON` |
| `created_at` | Creation timestamp |

## Callback envelope

Crypto events contain `event_id`, `event`, and `transaction`. Event names reflect the transaction kind and state, such as `crypto.receipt.available`.

Verify the signature against the raw body using the [webhook signing contract](/webhooks/). Deduplicate by `event_id` and apply each ledger effect once. A valid timestamp alone does not prevent repeated processing.

## Receipt accounting

Two transfers of the same amount from one customer produce separate receipts. Credit depends on actual received funds and the receipt’s saved fee. Deposit fees are snapshotted at first observation, rounded down to one micro-USDT, and are not repriced by callback retries.

Unfreezing a receipt moves it through confirmation, collection, or availability without creating a second credit.

---

# Webhooks & signatures

NiriPay sends HTTP `POST` requests with a JSON body to the `webhook_url` supplied during transaction creation.

Your webhook endpoint should:

- Be publicly accessible over HTTPS.
- Verify the signature and timestamp before parsing or processing the body.
- Respond with a `2xx` status within 10 seconds, only after durable acceptance or successful idempotent processing.
- Store the verified raw payload for audit.
- Locate the local transaction by `transaction_id`.
- Process duplicate events idempotently.
- Apply each balance change only once.
- Use status polling as a fallback.

## Verify webhook authenticity

Use your **existing merchant API key** as the signing secret. There is no separate webhook key, and the secret is never sent in the callback. Keep it on your server.

| Header | Format |
| --- | --- |
| `X-NiriPay-Timestamp` | Unix time in seconds, expressed as an integer string |
| `X-NiriPay-Signature` | HMAC-SHA256 digest encoded as 64 lowercase hexadecimal characters |

Calculate the expected signature as:

```text
HMAC-SHA256(key = merchant API key, message = timestamp + "." + raw request body)
```

Use the original timestamp header string and the **exact raw body bytes**, including whitespace and UTF-8 encoding. Do not parse and reserialize JSON before verification. Decode the hexadecimal signatures and compare equal-length buffers using a constant-time comparison.

Reject missing or malformed headers and invalid signatures. We recommend rejecting timestamps more than **300 seconds (five minutes) in the past or future** relative to your server clock. Keep the clock synchronized. This is recommended receiver-side validation, not a gateway-enforced policy. A valid timestamp does not prevent duplicate processing within that window: apply each balance change only once.

Signing uses the merchant's **current API key at delivery time**, rather than a key snapshotted when the transaction was created. Update your receiver configuration when changing that key. An in-flight callback signed before the change may fail verification with the new key; reconcile missed events using the status API.

Fiat deposit and withdrawal callbacks use the same signing contract. Crypto callbacks also use it, but their payload contains `event_id`, `event`, and `transaction`; deduplicate crypto events by `event_id`. Crypto delivery attempts are retried and signed again with a fresh timestamp and the current key. The current fiat delivery queue also retries unsuccessful deliveries with backoff and supplies an `X-NiriPay-Event-Id` header. The HMAC signs the timestamp and raw body, not this event-ID header; do not use that header alone to authorize a financial effect. Deduplicate verified transaction state changes and retain status reconciliation for both APIs.

## Deposit postback

Deposit postbacks are sent for successful payments.

```json
{
  "event": "deposit.status_updated",
  "data": {
    "transaction_id": "TXN123456789",
    "amount": 1000,
    "status": "success",
    "remarks": null,
    "fees": {
      "percentage": 3,
      "fixed_amount": 2,
      "total_amount": 32.0,
      "net_credit_amount": 968.0
    }
  }
}
```

| Field | Type | Description |
| --- | --- | --- |
| `event` | String | Always `deposit.status_updated` |
| `data.transaction_id` | String | Your deposit transaction ID |
| `data.amount` | Number | Deposit amount in INR |
| `data.status` | String | `success` for completed deposits |
| `data.remarks` | String or null | Additional transaction remarks |
| `data.fees` | Object or null | Saved order pricing; see Order fee details |

Use Get Deposit Status to recover expired, failed, or missing notifications.

## Withdrawal postback

Success:

```json
{
  "event": "withdrawal.status_updated",
  "data": {
    "transaction_id": "WD-2026-001",
    "amount": 5000,
    "status": "success",
    "proof": "UTR1234567890123456",
    "fees": {
      "percentage": 3,
      "fixed_amount": 2,
      "total_amount": 152.0,
      "total_debit_amount": 5152.0
    }
  }
}
```

Failure:

```json
{
  "event": "withdrawal.status_updated",
  "data": {
    "transaction_id": "WD-2026-001",
    "amount": 5000,
    "status": "failed",
    "proof": null,
    "fees": {
      "percentage": 3,
      "fixed_amount": 2,
      "total_amount": 152.0,
      "total_debit_amount": 5152.0
    }
  }
}
```

| Field | Type | Description |
| --- | --- | --- |
| `event` | String | Always `withdrawal.status_updated` |
| `data.transaction_id` | String | Your withdrawal transaction ID |
| `data.amount` | Number | Withdrawal amount in INR |
| `data.status` | String | `success` or `failed` |
| `data.proof` | String or null | Bank UTR for successful withdrawals |
| `data.fees` | Object or null | Saved order pricing, including after reversal; see Order fee details |

## Node.js webhook example

Install Express and use Node.js ES modules. Set `NIRIPAY_MERCHANT_API_KEY` on **your server** to your existing merchant API key; this is a merchant-side configuration name used by this example.

Register the webhook route with `express.raw()` **before** any global `express.json()` middleware. The example returns HTTP `401` for invalid authentication, `400` for a verified but invalid payload, and `500` if durable acceptance fails. It sends `200` only after the persistence hook succeeds.

**Implement `persistEventOnce` before use.** It deliberately throws until you replace it with your database transaction or durable queue. Ensure duplicate events do not repeat balance changes; a signature is not an event identifier. Do not acknowledge an event and then rely on an unpersisted background task to process it.

The handler validates fiat deposit and withdrawal payloads. For crypto, reuse `verifyWebhook` but replace `validFiatEvent` with validation of the crypto payload and deduplicate by `event_id`.

```js
import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";

const app = express();
// Set this on YOUR server to your existing NiriPay merchant API key.
const apiKey = process.env.NIRIPAY_MERCHANT_API_KEY;
if (!apiKey) throw new Error("Set NIRIPAY_MERCHANT_API_KEY before starting");

function verifyWebhook(rawBody, timestamp, signature, now = Date.now()) {
  if (!Buffer.isBuffer(rawBody) || typeof timestamp !== "string" ||
      !/^[0-9]+$/.test(timestamp) || typeof signature !== "string" ||
      !/^[a-f0-9]{64}$/.test(signature)) return false;

  const seconds = Number(timestamp);
  if (!Number.isSafeInteger(seconds) ||
      Math.abs(Math.floor(now / 1000) - seconds) > 300) return false;

  const expected = createHmac("sha256", apiKey)
    .update(timestamp + ".", "utf8")
    .update(rawBody)
    .digest();
  const received = Buffer.from(signature, "hex");
  return received.length === expected.length && timingSafeEqual(received, expected);
}

function validFiatEvent(payload) {
  const data = payload?.data;
  if (!data || typeof data.transaction_id !== "string" || !data.transaction_id ||
      typeof data.amount !== "number" || !Number.isFinite(data.amount) ||
      data.amount <= 0) return false;
  if (payload.event === "deposit.status_updated") {
    return data.status === "success" &&
      (data.remarks === null || typeof data.remarks === "string");
  }
  if (payload.event === "withdrawal.status_updated") {
    return ["success", "failed"].includes(data.status) &&
      (data.proof === null || typeof data.proof === "string");
  }
  return false;
}

async function persistEventOnce(payload) {
  // MERCHANT IMPLEMENTATION REQUIRED: durably store/queue the verified event
  // or finish processing it in a database transaction before resolving.
  // Reconcile with the status API and your stored order amount/currency.
  // Make duplicate deliveries no-ops and apply each balance change only once.
  // Do not deduplicate solely by signature: a repeated event can be re-signed.
  // Throw on persistence failure so the handler does not acknowledge success.
  void payload;
  throw new Error("Implement persistEventOnce using your database or durable queue");
}

// Register this route BEFORE app.use(express.json()) so req.body stays a Buffer.
app.post("/webhook", express.raw({ type: "application/json" }), async (req, res) => {
  if (!verifyWebhook(req.body, req.get("X-NiriPay-Timestamp"),
      req.get("X-NiriPay-Signature"))) return res.sendStatus(401);

  let payload;
  try {
    payload = JSON.parse(req.body.toString("utf8"));
  } catch {
    return res.sendStatus(400);
  }
  if (!validFiatEvent(payload)) return res.sendStatus(400);

  try {
    await persistEventOnce(payload);
    return res.sendStatus(200);
  } catch {
    return res.sendStatus(500);
  }
});

app.use(express.json()); // JSON parsing for your other routes goes AFTER /webhook.
```

---

# Fees & amounts

Deposit and withdrawal create/status responses and newly queued webhooks include
`data.fees`. All fields are JSON numbers; monetary values are INR, and a
`percentage` of `3` means 3%.

| Field inside `data.fees` | Meaning |
| --- | --- |
| `percentage` | Total merchant percentage rate |
| `fixed_amount` | Total fixed charge in INR |
| `total_amount` | Total fee, including percentage and fixed charges, in INR |
| `net_credit_amount` | Deposits only: order amount after fees, the expected merchant credit on success |
| `total_debit_amount` | Withdrawals only: order amount plus fees, the priced total merchant debit |

These fields describe **saved order pricing**, including while pending or after
failure. They do not confirm an actual charge, credit, or refund. For example,
a reversed payout can still show its original fee and total debit. Pricing is
not recalculated from current merchant settings.

Missing or incomplete saved pricing returns `"fees": null`, which does not mean
zero fees. Old cached create responses and previously queued webhooks retain
their original payloads and may omit `fees`; query the status API for available
saved pricing. Existing error responses do not gain fee fields.

## Currency rules

INR deposit amounts are JSON numbers between 100 and 50,000. INR payout amounts are whole-number JSON values between 100 and 100,000.

USDT uses decimal strings with up to six fractional digits. Its APIs and balances are separate from INR. See [USDT overview](/crypto-overview/).

Example fees in this documentation are illustrative, not a price offer. Your merchant agreement determines the applied rates.

---

# Retries & recovery

A transaction is not necessarily lost when a postback is delayed. Network failures, server downtime, and timeouts can interrupt delivery while the payment continues.

Recommended recovery flow:

1. Persist each create request and response.
2. Wait for the webhook as the primary notification method.
3. Poll the matching status API after the expected callback window.
4. Use capped exponential backoff.
5. Stop polling after a final state.
6. Reconcile long-running pending records through a scheduled operational job.

## Unknown create outcomes

A timeout does not prove a request failed. Keep the original transaction ID, look up its status, and investigate before issuing a replacement. An already-used ID is not permission to create another charge or transfer.

For USDT payouts, retain the original `idempotency_key`, `quote_id`, and callback URL. Reuse identical values when retrying the same request. See [USDT payouts](/crypto-payouts/).

---

# Errors & troubleshooting

## Authentication errors (`401`)

| Message | Resolution |
| --- | --- |
| `Unauthorized - Missing API Key` | Add `Authorization: Bearer YOUR_API_KEY` |
| `Unauthorized - Invalid API Key` | Copy the current key from the merchant dashboard and remove extra spaces |
| `Unauthorized - Merchant is not approved` | Complete merchant approval or contact support |
| `Unauthorized - IP not whitelisted` | Add the server egress IP to the whitelist |

## Deposit validation errors

| Message | Resolution |
| --- | --- |
| `amount: Amount must be at least 100` | Increase the deposit amount to at least INR 100 |
| `amount: Amount must not exceed 50000` | Reduce the deposit amount to INR 50,000 or less |
| `webhook_url: Invalid webhook URL format` | Send a valid public webhook URL |
| `Transaction ID already used` | Generate a new unique transaction ID |
| `No enabled gateways found` | Retry later or contact support if it persists |

## Withdrawal validation errors

| Message | Resolution |
| --- | --- |
| `amount: Amount must be an integer (no decimals)` | Send a whole-number INR amount |
| `amount: Amount must be at least 100` | Increase the amount to at least INR 100 |
| `amount: Amount must not exceed 100000` | Reduce the amount to INR 100,000 or less |
| `transaction_id: Transaction ID must be at least 7 characters` | Use at least 7 characters |
| `transaction_id: Transaction ID must not exceed 32 characters` | Use no more than 32 characters |
| `Transaction ID already used` | Generate a new unique transaction ID |
| `No valid gateway strategy` | No payout gateway supports the request; contact support if it persists |

## Status and evidence errors

| Message | Resolution |
| --- | --- |
| `Deposit not found` | Use the same merchant key and transaction ID that created the deposit |
| `Withdrawal not found` | Use the same merchant key and transaction ID that created the withdrawal |
| `transaction_id: Transaction ID is required` | Include `transaction_id` in the request |
| `UTR already submitted` | Do not repeat the UTR or screenshot submission |
| `UPI ID does not belong to this deposit` | Omit `upi_id` or use the UPI ID for the deposit payment slot |
| `Deposit is already success/failed/rejected` | Stop submitting evidence and read the current status |
| `PDF file is required` | Attach a PDF in the `pdf` field |
| `Video file is required` | Attach a video in the `video` field |
| `Documents upload is not required for this deposit` | Upload documents only in the `need_pdf_video` state |

## Support escalation information

When contacting support, include:

- Merchant `transaction_id`.
- NiriPay `internal_transaction_id`, when available.
- UTC timestamp and endpoint path.
- HTTP status and complete response body.
- Sanitized request fields.

Never include the merchant API key, bank credentials, or unnecessary personal data.

---

# Settlements & top-ups

## Request a settlement

The current merchant dashboard supports requesting USDT settlement of an eligible INR balance to a TRC20 wallet.

1. Select the merchant and open **Settlements**.
2. Enter the INR amount and destination TRC20 address.
3. Review the displayed conversion rate, fee, and estimated net USDT.
4. Submit the request and track its operational status.
5. Use the recorded transaction hash for reconciliation when completed.

Settlement timing, rates, limits, and availability depend on your merchant terms and configuration. Request acceptance does not mean the transfer is complete.

## USDT-funded INR top-ups

Where enabled, the INR top-up workflow requires admin approval before payment instructions are issued. The approved rate and receiving wallet are recorded for the request.

Submit the incoming transaction hash after funding. Operations verifies the receipt and gateway funding before crediting the INR wallet. This is an approved operational workflow, not an instant automatic exchange.

## Keep the balances separate

The INR top-up workflow is different from the [USDT wallet top-up API](/crypto-balances/). An INR balance cannot fund a USDT payout.

## Integration boundary

Use the merchant dashboard for these workflows. This guide does not advertise internal administration or session-only dashboard endpoints as merchant Bearer APIs.

---

# WooCommerce

The merchant dashboard **Resources** section provides the NiriPay WooCommerce gateway plugin.

Before enabling it:

- Set the WooCommerce store currency to INR.
- Configure the merchant API key server-side.
- Configure the webhook URL.
- Add the store server's egress IP to the merchant IP whitelist.
- Download the current plugin build from the dashboard so its configuration matches the deployed NiriPay gateway.

## Configure the integration

1. Download the plugin from the merchant dashboard’s Resources section.
2. In WordPress, open **Plugins → Add New → Upload Plugin** and activate the ZIP.
3. Open **WooCommerce → Settings → Payments → NiriPay**.
4. Configure the merchant credentials and production API base URL.
5. Select the available checkout presentation mode and order-status mapping.
6. Configure the plugin’s callback URL and server IP allowlist.

The plugin includes transaction inspection and payment-status refresh tools. Confirm compatibility with your installed WordPress and WooCommerce versions before launch.

## Verify before accepting orders

Check the store’s INR currency, checkout flow, callback handling, and order fulfillment rules. Keep credentials out of client-side scripts and shared troubleshooting logs.

---

# Go-live checklist

- Use a production HTTPS webhook that responds within 10 seconds.
- Keep API credentials in a server-side secret manager.
- Generate unique transaction IDs and enforce idempotency locally.
- Store all create responses before redirecting users or acknowledging requests.
- Reconcile webhook events with status polling.
- Validate amount limits before sending requests.
- Retain transaction and evidence logs according to your compliance policy.
- Test pending, success, failed, duplicate, and missing-webhook paths.

## Payment correctness

- Distinguish API acceptance from final payment success.
- Fulfill each order once, even when callbacks repeat.
- Treat unknown payout outcomes as unresolved rather than creating replacement transfers.
- Keep INR and USDT accounting separate.
- Treat sample fees, IDs, and beneficiary data as examples only.

## Evidence and callbacks

- Handle masked or unreadable screenshots without declaring them successful.
- Keep the raw webhook body available for signature verification.
- Reject invalid signatures and stale timestamps.
- Maintain status reconciliation when notifications are delayed.

## Support handoff

Include transaction identifiers, UTC timestamps, the endpoint, HTTP status, and sanitized response details when reporting an issue. Never share your API key.

---

