@thepayulink/server 2.0.1 → 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 CHANGED
@@ -22,8 +22,9 @@ Node 18+ (uses global `fetch`). Works with `import` and `require`.
22
22
  import PayuLink, { rupees } from '@thepayulink/server';
23
23
 
24
24
  const pl = new PayuLink({
25
- keyId: process.env.PAYULINK_KEY_ID, // pl_live_… / pl_test_…
26
- keySecret: process.env.PAYULINK_KEY_SECRET, // backend only — never ship this
25
+ keyId: process.env.PAYULINK_KEY_ID, // pl_live_… / pl_test_…
26
+ keySecret: process.env.PAYULINK_KEY_SECRET, // backend only — never ship this
27
+ webhookSecret: process.env.PAYULINK_WEBHOOK_SECRET, // whs_… from panel → Config (NOT keySecret)
27
28
  });
28
29
 
29
30
  // 1) Create an order for the TRUE amount, computed on your server.
@@ -64,6 +65,28 @@ toRupees(19950); // 199.5
64
65
  | `orders.isPaid(orderId)` | boolean | Convenience. |
65
66
  | `verifyPaymentSignature({ payulink_order_id, payulink_payment_id, payulink_signature })` | boolean | Verify the client success handoff. |
66
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.
67
90
 
68
91
  ### Idempotency
69
92
 
package/dist/index.d.ts CHANGED
@@ -7,8 +7,12 @@ export interface PayuLinkOptions {
7
7
  keySecret: string;
8
8
  /** Gateway base URL. Default https://payulink.io/api */
9
9
  apiBase?: string;
10
- /** Webhook signing secret (webhook_secret_v2). Falls back to keySecret. */
11
- webhookSecret?: string;
10
+ /**
11
+ * Webhook signing secret (webhook_secret_v2, whs_…) from merchant panel → Config.
12
+ * Required to verify webhooks — it is NOT your keySecret. Pass [newSecret, oldSecret]
13
+ * while rotating.
14
+ */
15
+ webhookSecret?: string | string[];
12
16
  /** Request timeout in ms. Default 15000. */
13
17
  timeout?: number;
14
18
  }
@@ -24,7 +28,10 @@ export interface CreateOrderParams {
24
28
  notes?: Record<string, unknown>;
25
29
  /** Optional customer prefill. */
26
30
  customer?: { name?: string; contact?: string; email?: string };
27
- /** Per-order webhook override. */
31
+ /**
32
+ * @deprecated Only affects legacy version-1 callbacks. Signed (v2) webhooks always go to
33
+ * the URL configured in the merchant panel.
34
+ */
28
35
  notifyUrl?: string;
29
36
  }
30
37
 
@@ -57,6 +64,81 @@ export interface PaymentHandoff {
57
64
  signature?: string;
58
65
  }
59
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
+
60
142
  export interface WebhookEvent {
61
143
  id?: string;
62
144
  event_type?: string;
@@ -87,12 +169,32 @@ export default class PayuLink {
87
169
  create(params: CreateOrderParams): Promise<Order>;
88
170
  fetch(orderId: string): Promise<Order>;
89
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>>;
90
190
  };
91
191
 
92
192
  webhooks: {
93
- verify(rawBody: string | Buffer, signature: string, timestamp: string | number, secret?: string): boolean;
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 }>;
195
+ verify(rawBody: string | Buffer, signature: string, timestamp: string | number, secret?: string | string[]): boolean;
94
196
  /** Verifies signature + ±5min timestamp tolerance. Throws PayuLinkError if invalid. */
95
- constructEvent(rawBody: string | Buffer, headers: Record<string, any>, secret?: string): WebhookEvent;
197
+ constructEvent(rawBody: string | Buffer, headers: Record<string, any>, secret?: string | string[]): WebhookEvent;
96
198
  };
97
199
 
98
200
  /** signature = HMAC_SHA256(order_id + "|" + payment_id, key_secret) */
@@ -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 ?? ""));
@@ -58,8 +68,10 @@ var PayuLink = class {
58
68
  * @param {string} opts.keyId Publishable key (pl_live_… / pl_test_…).
59
69
  * @param {string} opts.keySecret Secret key. BACKEND ONLY — never ship this.
60
70
  * @param {string} [opts.apiBase] Default https://payulink.io/api
61
- * @param {string} [opts.webhookSecret] Webhook signing secret (webhook_secret_v2 from your
62
- * merchant panel). Falls back to keySecret.
71
+ * @param {string|string[]} [opts.webhookSecret] Webhook signing secret (webhook_secret_v2,
72
+ * whs_…, from merchant panel → Config). Required to verify
73
+ * webhooks. Pass [new, old] while rotating — see constructEvent.
74
+ * It is NOT your keySecret: the two are different secrets.
63
75
  * @param {number} [opts.timeout] Request timeout ms. Default 15000.
64
76
  */
65
77
  constructor(opts = {}) {
@@ -68,15 +80,33 @@ var PayuLink = class {
68
80
  this.keyId = opts.keyId;
69
81
  this.keySecret = opts.keySecret;
70
82
  this.apiBase = String(opts.apiBase || "https://payulink.io/api").replace(/\/$/, "");
71
- this.webhookSecret = opts.webhookSecret || opts.keySecret;
83
+ this.webhookSecret = opts.webhookSecret || null;
72
84
  this.timeout = opts.timeout || 15e3;
73
85
  this.mode = /^pl_test_/.test(opts.keyId) ? "test" : "live";
74
86
  this.orders = {
75
87
  create: (params) => this._createOrder(params),
76
88
  fetch: (orderId) => this._fetchOrder(orderId),
77
- 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))
78
107
  };
79
108
  this.webhooks = {
109
+ test: () => this._request("POST", "/v1/webhooks/test"),
80
110
  verify: (rawBody, signature, timestamp, secret) => this._verifyWebhook(rawBody, signature, timestamp, secret),
81
111
  constructEvent: (rawBody, headers, secret) => this._constructEvent(rawBody, headers, secret)
82
112
  };
@@ -125,6 +155,10 @@ var PayuLink = class {
125
155
  * @param {string} [p.receipt] Your reference. Unique per merchant — reusing it
126
156
  * returns the SAME order, which makes retries safe.
127
157
  * @param {object} [p.notes] Arbitrary key/value metadata.
158
+ * @param {object} [p.customer] { name, contact, email } — prefill for the checkout.
159
+ *
160
+ * Webhooks always go to the URL set in the merchant panel. (`notifyUrl` is still sent for
161
+ * backward compatibility but only affects legacy version-1 callbacks, not signed webhooks.)
128
162
  */
129
163
  _createOrder(p = {}) {
130
164
  const amount = Math.round(Number(p.amount));
@@ -144,6 +178,33 @@ var PayuLink = class {
144
178
  }
145
179
  });
146
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
+ }
147
208
  /** Authoritative order status. */
148
209
  _fetchOrder(orderId) {
149
210
  if (!orderId) throw new PayuLinkError("orderId is required");
@@ -176,9 +237,11 @@ var PayuLink = class {
176
237
  */
177
238
  _verifyWebhook(rawBody, signature, timestamp, secret) {
178
239
  if (!signature || !timestamp) return false;
179
- const key = secret || this.webhookSecret;
180
- const expected = "sha256=" + import_node_crypto.default.createHmac("sha256", key).update(`${timestamp}.${String(rawBody)}`).digest("hex");
181
- return safeEqual(expected, signature);
240
+ const keys = [].concat(secret || this.webhookSecret || []).filter(Boolean);
241
+ if (keys.length === 0) {
242
+ throw new PayuLinkError("No webhook secret: pass `webhookSecret` (whs_\u2026, merchant panel \u2192 Config) to new PayuLink({...}). Webhooks are not signed with your keySecret.");
243
+ }
244
+ return keys.some((key) => safeEqual("sha256=" + import_node_crypto.default.createHmac("sha256", key).update(`${timestamp}.${String(rawBody)}`).digest("hex"), signature));
182
245
  }
183
246
  /**
184
247
  * Verify + parse a webhook. Throws if the signature is bad or the timestamp is
@@ -194,7 +257,7 @@ var PayuLink = class {
194
257
  *
195
258
  * @param {string|Buffer} rawBody
196
259
  * @param {object} headers The request headers object.
197
- * @param {string} [secret]
260
+ * @param {string|string[]} [secret] Overrides opts.webhookSecret.
198
261
  * @param {number} [toleranceSec=300]
199
262
  */
200
263
  _constructEvent(rawBody, headers = {}, secret, toleranceSec = 300) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thepayulink/server",
3
- "version": "2.0.1",
3
+ "version": "2.2.0",
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 ?? ''));
@@ -41,8 +53,10 @@ export default class PayuLink {
41
53
  * @param {string} opts.keyId Publishable key (pl_live_… / pl_test_…).
42
54
  * @param {string} opts.keySecret Secret key. BACKEND ONLY — never ship this.
43
55
  * @param {string} [opts.apiBase] Default https://payulink.io/api
44
- * @param {string} [opts.webhookSecret] Webhook signing secret (webhook_secret_v2 from your
45
- * merchant panel). Falls back to keySecret.
56
+ * @param {string|string[]} [opts.webhookSecret] Webhook signing secret (webhook_secret_v2,
57
+ * whs_…, from merchant panel → Config). Required to verify
58
+ * webhooks. Pass [new, old] while rotating — see constructEvent.
59
+ * It is NOT your keySecret: the two are different secrets.
46
60
  * @param {number} [opts.timeout] Request timeout ms. Default 15000.
47
61
  */
48
62
  constructor(opts = {}) {
@@ -51,7 +65,9 @@ export default class PayuLink {
51
65
  this.keyId = opts.keyId;
52
66
  this.keySecret = opts.keySecret;
53
67
  this.apiBase = String(opts.apiBase || 'https://payulink.io/api').replace(/\/$/, '');
54
- this.webhookSecret = opts.webhookSecret || opts.keySecret;
68
+ // No fallback to keySecret. Webhooks are never signed with it, so the old fallback
69
+ // turned a missing env var into "every webhook fails signature" with no clue why.
70
+ this.webhookSecret = opts.webhookSecret || null;
55
71
  this.timeout = opts.timeout || 15000;
56
72
  this.mode = /^pl_test_/.test(opts.keyId) ? 'test' : 'live';
57
73
 
@@ -59,8 +75,24 @@ export default class PayuLink {
59
75
  create: (params) => this._createOrder(params),
60
76
  fetch: (orderId) => this._fetchOrder(orderId),
61
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)),
62
93
  };
63
94
  this.webhooks = {
95
+ test: () => this._request('POST', '/v1/webhooks/test'),
64
96
  verify: (rawBody, signature, timestamp, secret) =>
65
97
  this._verifyWebhook(rawBody, signature, timestamp, secret),
66
98
  constructEvent: (rawBody, headers, secret) => this._constructEvent(rawBody, headers, secret),
@@ -109,6 +141,10 @@ export default class PayuLink {
109
141
  * @param {string} [p.receipt] Your reference. Unique per merchant — reusing it
110
142
  * returns the SAME order, which makes retries safe.
111
143
  * @param {object} [p.notes] Arbitrary key/value metadata.
144
+ * @param {object} [p.customer] { name, contact, email } — prefill for the checkout.
145
+ *
146
+ * Webhooks always go to the URL set in the merchant panel. (`notifyUrl` is still sent for
147
+ * backward compatibility but only affects legacy version-1 callbacks, not signed webhooks.)
112
148
  */
113
149
  _createOrder(p = {}) {
114
150
  const amount = Math.round(Number(p.amount));
@@ -130,6 +166,28 @@ export default class PayuLink {
130
166
  });
131
167
  }
132
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
+
133
191
  /** Authoritative order status. */
134
192
  _fetchOrder(orderId) {
135
193
  if (!orderId) throw new PayuLinkError('orderId is required');
@@ -165,10 +223,16 @@ export default class PayuLink {
165
223
  */
166
224
  _verifyWebhook(rawBody, signature, timestamp, secret) {
167
225
  if (!signature || !timestamp) return false;
168
- const key = secret || this.webhookSecret;
169
- const expected = 'sha256=' + crypto.createHmac('sha256', key)
170
- .update(`${timestamp}.${String(rawBody)}`).digest('hex');
171
- return safeEqual(expected, signature);
226
+ const keys = [].concat(secret || this.webhookSecret || []).filter(Boolean);
227
+ if (keys.length === 0) {
228
+ throw new PayuLinkError('No webhook secret: pass `webhookSecret` (whs_…, merchant panel → Config) '
229
+ + 'to new PayuLink({...}). Webhooks are not signed with your keySecret.');
230
+ }
231
+ // Several secrets are accepted so a rotation never drops events: deliveries already
232
+ // queued (and their retries, for up to ~30h) stay signed with the secret that was
233
+ // current when each event was created.
234
+ return keys.some((key) => safeEqual('sha256=' + crypto.createHmac('sha256', key)
235
+ .update(`${timestamp}.${String(rawBody)}`).digest('hex'), signature));
172
236
  }
173
237
 
174
238
  /**
@@ -185,7 +249,7 @@ export default class PayuLink {
185
249
  *
186
250
  * @param {string|Buffer} rawBody
187
251
  * @param {object} headers The request headers object.
188
- * @param {string} [secret]
252
+ * @param {string|string[]} [secret] Overrides opts.webhookSecret.
189
253
  * @param {number} [toleranceSec=300]
190
254
  */
191
255
  _constructEvent(rawBody, headers = {}, secret, toleranceSec = 300) {