@thepayulink/server 2.1.0 → 2.2.0
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 +22 -0
- package/dist/index.d.ts +95 -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,28 @@ 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
|
+
```js
|
|
79
|
+
const payout = await pl.payouts.create({
|
|
80
|
+
amount: rupees(2500), // PAISE; UPI payouts must be whole rupees
|
|
81
|
+
mode: 'bank', // or 'upi' with beneficiary: { vpa }
|
|
82
|
+
receipt: 'withdrawal_981', // idempotency key
|
|
83
|
+
beneficiary: { name: 'Ravi Kumar', account_number: '918020012345678', ifsc: 'UTIB0000123' },
|
|
84
|
+
});
|
|
85
|
+
// -> { id: 'ZC…', status: 'pending', … } the amount is locked from your available balance
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Statuses: `pending` → `processing` → `processed`, or `cancelled` / `failed` / `rejected`.
|
|
89
|
+
Listen for `payout.processing`, `payout.completed` and `payout.cancelled` webhooks.
|
|
68
90
|
|
|
69
91
|
### Idempotency
|
|
70
92
|
|
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,29 @@ 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
|
+
payouts: {
|
|
176
|
+
create(params: CreatePayoutParams): Promise<Payout>;
|
|
177
|
+
fetch(payoutId: string): Promise<Payout>;
|
|
178
|
+
list(params?: ListParams & { status?: Payout['status']; mode?: 'bank' | 'upi'; receipt?: string; utr?: string }): Promise<Collection<Payout>>;
|
|
179
|
+
/** Bank: cancels a pending payout. UPI: files a cancel request (HTTP 202, cancel_requested: true). */
|
|
180
|
+
cancel(payoutId: string, params?: { reason?: string }): Promise<Payout>;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
balance: {
|
|
184
|
+
fetch(): Promise<Balance>;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
transactions: {
|
|
188
|
+
/** type: comma-separated names, e.g. "payout_lock,payout_debit" */
|
|
189
|
+
list(params?: ListParams & { type?: string }): Promise<Collection<Transaction>>;
|
|
97
190
|
};
|
|
98
191
|
|
|
99
192
|
webhooks: {
|
|
193
|
+
/** Sends a webhook.test event to your callback URL. */
|
|
194
|
+
test(): Promise<{ entity: 'webhook_test'; status: 'queued' | 'not_sent'; event_id?: string; reason?: string }>;
|
|
100
195
|
verify(rawBody: string | Buffer, signature: string, timestamp: string | number, secret?: string | string[]): boolean;
|
|
101
196
|
/** Verifies signature + ±5min timestamp tolerance. Throws PayuLinkError if invalid. */
|
|
102
197
|
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');
|