@thepayulink/server 2.1.0 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -0
- package/dist/index.d.ts +98 -0
- package/dist/payulink-server.cjs +56 -1
- package/package.json +1 -1
- package/src/index.js +50 -0
package/README.md
CHANGED
|
@@ -65,6 +65,34 @@ toRupees(19950); // 199.5
|
|
|
65
65
|
| `orders.isPaid(orderId)` | boolean | Convenience. |
|
|
66
66
|
| `verifyPaymentSignature({ payulink_order_id, payulink_payment_id, payulink_signature })` | boolean | Verify the client success handoff. |
|
|
67
67
|
| `webhooks.constructEvent(rawBody, headers)` | event | Verify + parse a webhook (throws if invalid or stale). |
|
|
68
|
+
| `webhooks.test()` | result | Send a `webhook.test` event to your callback URL. |
|
|
69
|
+
| `orders.list({ count?, skip?, status?, receipt?, utr?, from?, to? })` | collection | List orders, e.g. find one by UTR. |
|
|
70
|
+
| `payouts.create({ amount, mode, beneficiary, receipt?, narration? })` | payout | Pay out from your balance (`mode`: `bank` or `upi`). |
|
|
71
|
+
| `payouts.fetch(id)` / `payouts.list({...})` | payout / collection | Payout status. |
|
|
72
|
+
| `payouts.cancel(id)` | payout | Cancel a pending payout (bank: immediate; UPI: cancel request). |
|
|
73
|
+
| `balance.fetch()` | balance | `available`, `locked`, `frozen`, `total` in paise. |
|
|
74
|
+
| `transactions.list({ count?, skip?, type?, from?, to? })` | collection | Every balance movement. |
|
|
75
|
+
|
|
76
|
+
### Payouts
|
|
77
|
+
|
|
78
|
+
Payouts, `balance.fetch()` and `transactions.list()` need a **live key with Payouts access**
|
|
79
|
+
and an **IP allowlist** (merchant panel → API Key → *Edit access*). Otherwise the API
|
|
80
|
+
answers `403 KEY_NOT_PERMITTED`, `403 IP_NOT_ALLOWED`, or `400 TEST_MODE_UNSUPPORTED` for a
|
|
81
|
+
test key. Keep that key only on the server that sends payouts; use a separate Checkout key
|
|
82
|
+
for orders.
|
|
83
|
+
|
|
84
|
+
```js
|
|
85
|
+
const payout = await pl.payouts.create({
|
|
86
|
+
amount: rupees(2500), // PAISE; UPI payouts must be whole rupees
|
|
87
|
+
mode: 'bank', // or 'upi' with beneficiary: { vpa }
|
|
88
|
+
receipt: 'withdrawal_981', // idempotency key
|
|
89
|
+
beneficiary: { name: 'Ravi Kumar', account_number: '918020012345678', ifsc: 'UTIB0000123' },
|
|
90
|
+
});
|
|
91
|
+
// -> { id: 'ZC…', status: 'pending', … } the amount is locked from your available balance
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Statuses: `pending` → `processing` → `processed`, or `cancelled` / `failed` / `rejected`.
|
|
95
|
+
Listen for `payout.processing`, `payout.completed` and `payout.cancelled` webhooks.
|
|
68
96
|
|
|
69
97
|
### Idempotency
|
|
70
98
|
|
package/dist/index.d.ts
CHANGED
|
@@ -64,6 +64,81 @@ export interface PaymentHandoff {
|
|
|
64
64
|
signature?: string;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
export interface ListParams {
|
|
68
|
+
/** 1–100, default 10 */
|
|
69
|
+
count?: number;
|
|
70
|
+
skip?: number;
|
|
71
|
+
/** epoch seconds */
|
|
72
|
+
from?: number;
|
|
73
|
+
to?: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface Collection<T> {
|
|
77
|
+
entity: 'collection';
|
|
78
|
+
count: number;
|
|
79
|
+
has_more: boolean;
|
|
80
|
+
items: T[];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface PayoutBeneficiary {
|
|
84
|
+
name?: string;
|
|
85
|
+
account_number?: string;
|
|
86
|
+
ifsc?: string;
|
|
87
|
+
vpa?: string;
|
|
88
|
+
mobile?: string;
|
|
89
|
+
email?: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface CreatePayoutParams {
|
|
93
|
+
/** PAISE. UPI payouts must be whole rupees. */
|
|
94
|
+
amount: number;
|
|
95
|
+
mode: 'bank' | 'upi';
|
|
96
|
+
/** bank: name + account_number + ifsc · upi: vpa (name optional) */
|
|
97
|
+
beneficiary: PayoutBeneficiary;
|
|
98
|
+
receipt?: string;
|
|
99
|
+
narration?: string;
|
|
100
|
+
currency?: 'INR';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface Payout {
|
|
104
|
+
/** ZC… (bank) or MR-… (UPI) */
|
|
105
|
+
id: string;
|
|
106
|
+
entity: 'payout';
|
|
107
|
+
mode: 'bank' | 'upi';
|
|
108
|
+
amount: number;
|
|
109
|
+
currency: string;
|
|
110
|
+
receipt: string | null;
|
|
111
|
+
status: 'pending' | 'processing' | 'processed' | 'cancelled' | 'failed' | 'rejected' | 'reversed';
|
|
112
|
+
beneficiary: PayoutBeneficiary;
|
|
113
|
+
narration: string | null;
|
|
114
|
+
utr: string | null;
|
|
115
|
+
cancel_requested?: boolean;
|
|
116
|
+
created_at: number;
|
|
117
|
+
processed_at: number | null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface Balance {
|
|
121
|
+
entity: 'balance';
|
|
122
|
+
currency: string;
|
|
123
|
+
available: number;
|
|
124
|
+
locked: number;
|
|
125
|
+
frozen: number;
|
|
126
|
+
total: number;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface Transaction {
|
|
130
|
+
id: string;
|
|
131
|
+
entity: 'transaction';
|
|
132
|
+
type: string;
|
|
133
|
+
direction: 'credit' | 'debit' | 'lock' | 'locked_spent' | 'other';
|
|
134
|
+
amount: number;
|
|
135
|
+
currency: string;
|
|
136
|
+
available_before: number | null;
|
|
137
|
+
available_after: number | null;
|
|
138
|
+
description: string | null;
|
|
139
|
+
created_at: number;
|
|
140
|
+
}
|
|
141
|
+
|
|
67
142
|
export interface WebhookEvent {
|
|
68
143
|
id?: string;
|
|
69
144
|
event_type?: string;
|
|
@@ -94,9 +169,32 @@ export default class PayuLink {
|
|
|
94
169
|
create(params: CreateOrderParams): Promise<Order>;
|
|
95
170
|
fetch(orderId: string): Promise<Order>;
|
|
96
171
|
isPaid(orderId: string): Promise<boolean>;
|
|
172
|
+
list(params?: ListParams & { status?: string; receipt?: string; utr?: string }): Promise<Collection<Order>>;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
/** Needs a live key with Payouts access, called from an IP on the key's allowlist. */
|
|
176
|
+
payouts: {
|
|
177
|
+
create(params: CreatePayoutParams): Promise<Payout>;
|
|
178
|
+
fetch(payoutId: string): Promise<Payout>;
|
|
179
|
+
list(params?: ListParams & { status?: Payout['status']; mode?: 'bank' | 'upi'; receipt?: string; utr?: string }): Promise<Collection<Payout>>;
|
|
180
|
+
/** Bank: cancels a pending payout. UPI: files a cancel request (HTTP 202, cancel_requested: true). */
|
|
181
|
+
cancel(payoutId: string, params?: { reason?: string }): Promise<Payout>;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
/** Needs a live key with Payouts access, called from an IP on the key's allowlist. */
|
|
185
|
+
balance: {
|
|
186
|
+
fetch(): Promise<Balance>;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/** Needs a live key with Payouts access, called from an IP on the key's allowlist. */
|
|
190
|
+
transactions: {
|
|
191
|
+
/** type: comma-separated names, e.g. "payout_lock,payout_debit" */
|
|
192
|
+
list(params?: ListParams & { type?: string }): Promise<Collection<Transaction>>;
|
|
97
193
|
};
|
|
98
194
|
|
|
99
195
|
webhooks: {
|
|
196
|
+
/** Sends a webhook.test event to your callback URL. */
|
|
197
|
+
test(): Promise<{ entity: 'webhook_test'; status: 'queued' | 'not_sent'; event_id?: string; reason?: string }>;
|
|
100
198
|
verify(rawBody: string | Buffer, signature: string, timestamp: string | number, secret?: string | string[]): boolean;
|
|
101
199
|
/** Verifies signature + ±5min timestamp tolerance. Throws PayuLinkError if invalid. */
|
|
102
200
|
constructEvent(rawBody: string | Buffer, headers: Record<string, any>, secret?: string | string[]): WebhookEvent;
|
package/dist/payulink-server.cjs
CHANGED
|
@@ -47,6 +47,16 @@ var PayuLinkError = class extends Error {
|
|
|
47
47
|
this.body = body;
|
|
48
48
|
}
|
|
49
49
|
};
|
|
50
|
+
function query(params = {}) {
|
|
51
|
+
const q = new URLSearchParams();
|
|
52
|
+
for (const [k, v] of Object.entries(params || {})) if (v !== void 0 && v !== null && v !== "") q.set(k, String(v));
|
|
53
|
+
const s = q.toString();
|
|
54
|
+
return s ? `?${s}` : "";
|
|
55
|
+
}
|
|
56
|
+
function requireId(id, name) {
|
|
57
|
+
if (!id) throw new PayuLinkError(`${name} is required`);
|
|
58
|
+
return id;
|
|
59
|
+
}
|
|
50
60
|
function safeEqual(a, b) {
|
|
51
61
|
const ba = Buffer.from(String(a ?? ""));
|
|
52
62
|
const bb = Buffer.from(String(b ?? ""));
|
|
@@ -76,9 +86,27 @@ var PayuLink = class {
|
|
|
76
86
|
this.orders = {
|
|
77
87
|
create: (params) => this._createOrder(params),
|
|
78
88
|
fetch: (orderId) => this._fetchOrder(orderId),
|
|
79
|
-
isPaid: async (orderId) => (await this._fetchOrder(orderId)).status === "paid"
|
|
89
|
+
isPaid: async (orderId) => (await this._fetchOrder(orderId)).status === "paid",
|
|
90
|
+
list: (params) => this._request("GET", "/v1/orders" + query(params))
|
|
91
|
+
};
|
|
92
|
+
this.payouts = {
|
|
93
|
+
create: (params) => this._createPayout(params),
|
|
94
|
+
fetch: (payoutId) => this._request("GET", `/v1/payouts/${encodeURIComponent(requireId(payoutId, "payoutId"))}`),
|
|
95
|
+
list: (params) => this._request("GET", "/v1/payouts" + query(params)),
|
|
96
|
+
cancel: (payoutId, params = {}) => this._request(
|
|
97
|
+
"POST",
|
|
98
|
+
`/v1/payouts/${encodeURIComponent(requireId(payoutId, "payoutId"))}/cancel`,
|
|
99
|
+
{ body: params }
|
|
100
|
+
)
|
|
101
|
+
};
|
|
102
|
+
this.balance = {
|
|
103
|
+
fetch: () => this._request("GET", "/v1/balance")
|
|
104
|
+
};
|
|
105
|
+
this.transactions = {
|
|
106
|
+
list: (params) => this._request("GET", "/v1/transactions" + query(params))
|
|
80
107
|
};
|
|
81
108
|
this.webhooks = {
|
|
109
|
+
test: () => this._request("POST", "/v1/webhooks/test"),
|
|
82
110
|
verify: (rawBody, signature, timestamp, secret) => this._verifyWebhook(rawBody, signature, timestamp, secret),
|
|
83
111
|
constructEvent: (rawBody, headers, secret) => this._constructEvent(rawBody, headers, secret)
|
|
84
112
|
};
|
|
@@ -150,6 +178,33 @@ var PayuLink = class {
|
|
|
150
178
|
}
|
|
151
179
|
});
|
|
152
180
|
}
|
|
181
|
+
/**
|
|
182
|
+
* Pay money out from your PayuLink balance. The amount is locked immediately.
|
|
183
|
+
* @param {object} p
|
|
184
|
+
* @param {number} p.amount PAISE. UPI payouts must be whole rupees.
|
|
185
|
+
* @param {'bank'|'upi'} p.mode
|
|
186
|
+
* @param {object} p.beneficiary bank: { name, account_number, ifsc } · upi: { vpa, name? } · optional mobile, email
|
|
187
|
+
* @param {string} [p.receipt] Your reference; reusing it returns the same payout.
|
|
188
|
+
* @param {string} [p.narration]
|
|
189
|
+
*/
|
|
190
|
+
_createPayout(p = {}) {
|
|
191
|
+
const amount = Math.round(Number(p.amount));
|
|
192
|
+
if (!Number.isFinite(amount) || amount < 100) {
|
|
193
|
+
throw new PayuLinkError("`amount` is required, in PAISE, and must be >= 100 (\u20B91).");
|
|
194
|
+
}
|
|
195
|
+
if (p.mode !== "bank" && p.mode !== "upi") throw new PayuLinkError('`mode` must be "bank" or "upi".');
|
|
196
|
+
if (!p.beneficiary) throw new PayuLinkError("`beneficiary` is required.");
|
|
197
|
+
return this._request("POST", "/v1/payouts", {
|
|
198
|
+
body: {
|
|
199
|
+
amount,
|
|
200
|
+
currency: p.currency || "INR",
|
|
201
|
+
mode: p.mode,
|
|
202
|
+
beneficiary: p.beneficiary,
|
|
203
|
+
receipt: p.receipt,
|
|
204
|
+
narration: p.narration
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}
|
|
153
208
|
/** Authoritative order status. */
|
|
154
209
|
_fetchOrder(orderId) {
|
|
155
210
|
if (!orderId) throw new PayuLinkError("orderId is required");
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -29,6 +29,18 @@ export class PayuLinkError extends Error {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
function query(params = {}) {
|
|
33
|
+
const q = new URLSearchParams();
|
|
34
|
+
for (const [k, v] of Object.entries(params || {})) if (v !== undefined && v !== null && v !== '') q.set(k, String(v));
|
|
35
|
+
const s = q.toString();
|
|
36
|
+
return s ? `?${s}` : '';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function requireId(id, name) {
|
|
40
|
+
if (!id) throw new PayuLinkError(`${name} is required`);
|
|
41
|
+
return id;
|
|
42
|
+
}
|
|
43
|
+
|
|
32
44
|
function safeEqual(a, b) {
|
|
33
45
|
const ba = Buffer.from(String(a ?? ''));
|
|
34
46
|
const bb = Buffer.from(String(b ?? ''));
|
|
@@ -63,8 +75,24 @@ export default class PayuLink {
|
|
|
63
75
|
create: (params) => this._createOrder(params),
|
|
64
76
|
fetch: (orderId) => this._fetchOrder(orderId),
|
|
65
77
|
isPaid: async (orderId) => (await this._fetchOrder(orderId)).status === 'paid',
|
|
78
|
+
list: (params) => this._request('GET', '/v1/orders' + query(params)),
|
|
79
|
+
};
|
|
80
|
+
// Payouts, balance and transactions replace the legacy /openapi payout + acct endpoints.
|
|
81
|
+
this.payouts = {
|
|
82
|
+
create: (params) => this._createPayout(params),
|
|
83
|
+
fetch: (payoutId) => this._request('GET', `/v1/payouts/${encodeURIComponent(requireId(payoutId, 'payoutId'))}`),
|
|
84
|
+
list: (params) => this._request('GET', '/v1/payouts' + query(params)),
|
|
85
|
+
cancel: (payoutId, params = {}) => this._request('POST',
|
|
86
|
+
`/v1/payouts/${encodeURIComponent(requireId(payoutId, 'payoutId'))}/cancel`, { body: params }),
|
|
87
|
+
};
|
|
88
|
+
this.balance = {
|
|
89
|
+
fetch: () => this._request('GET', '/v1/balance'),
|
|
90
|
+
};
|
|
91
|
+
this.transactions = {
|
|
92
|
+
list: (params) => this._request('GET', '/v1/transactions' + query(params)),
|
|
66
93
|
};
|
|
67
94
|
this.webhooks = {
|
|
95
|
+
test: () => this._request('POST', '/v1/webhooks/test'),
|
|
68
96
|
verify: (rawBody, signature, timestamp, secret) =>
|
|
69
97
|
this._verifyWebhook(rawBody, signature, timestamp, secret),
|
|
70
98
|
constructEvent: (rawBody, headers, secret) => this._constructEvent(rawBody, headers, secret),
|
|
@@ -138,6 +166,28 @@ export default class PayuLink {
|
|
|
138
166
|
});
|
|
139
167
|
}
|
|
140
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Pay money out from your PayuLink balance. The amount is locked immediately.
|
|
171
|
+
* @param {object} p
|
|
172
|
+
* @param {number} p.amount PAISE. UPI payouts must be whole rupees.
|
|
173
|
+
* @param {'bank'|'upi'} p.mode
|
|
174
|
+
* @param {object} p.beneficiary bank: { name, account_number, ifsc } · upi: { vpa, name? } · optional mobile, email
|
|
175
|
+
* @param {string} [p.receipt] Your reference; reusing it returns the same payout.
|
|
176
|
+
* @param {string} [p.narration]
|
|
177
|
+
*/
|
|
178
|
+
_createPayout(p = {}) {
|
|
179
|
+
const amount = Math.round(Number(p.amount));
|
|
180
|
+
if (!Number.isFinite(amount) || amount < 100) {
|
|
181
|
+
throw new PayuLinkError('`amount` is required, in PAISE, and must be >= 100 (₹1).');
|
|
182
|
+
}
|
|
183
|
+
if (p.mode !== 'bank' && p.mode !== 'upi') throw new PayuLinkError('`mode` must be "bank" or "upi".');
|
|
184
|
+
if (!p.beneficiary) throw new PayuLinkError('`beneficiary` is required.');
|
|
185
|
+
return this._request('POST', '/v1/payouts', {
|
|
186
|
+
body: { amount, currency: p.currency || 'INR', mode: p.mode, beneficiary: p.beneficiary,
|
|
187
|
+
receipt: p.receipt, narration: p.narration },
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
141
191
|
/** Authoritative order status. */
|
|
142
192
|
_fetchOrder(orderId) {
|
|
143
193
|
if (!orderId) throw new PayuLinkError('orderId is required');
|