@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 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;
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thepayulink/server",
3
- "version": "2.1.0",
3
+ "version": "2.2.1",
4
4
  "description": "PayuLink server SDK — create orders and verify payments/webhooks with your key_secret.",
5
5
  "type": "module",
6
6
  "main": "./dist/payulink-server.cjs",
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');