@secureprocessing/node 0.0.0-stage → 0.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/LICENSE +21 -0
- package/README.md +248 -2
- package/dist/client.d.ts +94 -0
- package/dist/client.js +196 -0
- package/dist/errors.d.ts +99 -0
- package/dist/errors.js +125 -0
- package/dist/index.d.ts +603 -0
- package/dist/index.js +520 -0
- package/dist/pagination.d.ts +32 -0
- package/dist/pagination.js +52 -0
- package/dist/types.d.ts +892 -0
- package/dist/types.js +3 -0
- package/dist/version.d.ts +8 -0
- package/dist/version.js +9 -0
- package/dist/webhooks.d.ts +22 -0
- package/dist/webhooks.js +79 -0
- package/package.json +32 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
// @ts-self-types="./index.d.ts"
|
|
2
|
+
import { HttpClient } from "./client.js";
|
|
3
|
+
import { constructEvent, parseSignatureHeader, SIGNATURE_HEADER } from "./webhooks.js";
|
|
4
|
+
export * from "./types.js";
|
|
5
|
+
export * from "./errors.js";
|
|
6
|
+
export { constructEvent, parseSignatureHeader, SIGNATURE_HEADER };
|
|
7
|
+
export { VERSION } from "./version.js";
|
|
8
|
+
export { autoPaginate } from "./pagination.js";
|
|
9
|
+
/**
|
|
10
|
+
* The Secure Processing API client.
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* const sp = new SecureProcessing(process.env.SECURE_PROCESSING_KEY!);
|
|
14
|
+
*
|
|
15
|
+
* const intent = await sp.paymentIntents.create({
|
|
16
|
+
* amount: 6000, // minor units — $60.00
|
|
17
|
+
* currency: "usd",
|
|
18
|
+
* description: "SCU-B0161 — Blue Hole Two-Tank",
|
|
19
|
+
* metadata: { shop_id: "scuba-sensation" },
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* const session = await sp.checkoutSessions.create({
|
|
23
|
+
* payment_intent: intent.id,
|
|
24
|
+
* success_url: "https://example.com/thanks",
|
|
25
|
+
* });
|
|
26
|
+
* // Send the customer to session.url
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* Runs on Node 20+, Deno, Bun and Cloudflare Workers: it has no dependencies and uses only
|
|
30
|
+
* `fetch` and Web Crypto.
|
|
31
|
+
*/
|
|
32
|
+
export class SecureProcessing {
|
|
33
|
+
http;
|
|
34
|
+
constructor(apiKey, options = {}) {
|
|
35
|
+
this.http = new HttpClient(apiKey, options);
|
|
36
|
+
}
|
|
37
|
+
paymentIntents = {
|
|
38
|
+
/**
|
|
39
|
+
* Create a payment intent, and with `confirm: true` charge a saved card now.
|
|
40
|
+
*
|
|
41
|
+
* 🔴 A confirmed intent that comes back `processing` was sent to the bank and its answer has not
|
|
42
|
+
* reached us yet. The card may have been charged: do not create another payment for it. It
|
|
43
|
+
* settles on its own to `succeeded` or `requires_payment_method`, and the webhook follows. A
|
|
44
|
+
* thrown `SecureProcessingError` with `isOutcomeUnknown` means the same: check `status` on a
|
|
45
|
+
* `retrieve`, and never repeat the charge under a new idempotency key.
|
|
46
|
+
*/
|
|
47
|
+
create: (params, options) => this.http.post("/v1/payment_intents", params, options),
|
|
48
|
+
retrieve: (id) => this.http.get(`/v1/payment_intents/${encodeURIComponent(id)}`),
|
|
49
|
+
list: (params) => this.http.get("/v1/payment_intents", params),
|
|
50
|
+
/** Settle a payment created with `capture_method: "manual"`. */
|
|
51
|
+
capture: (id, params = {}, options) => this.http.post(`/v1/payment_intents/${encodeURIComponent(id)}/capture`, params, options),
|
|
52
|
+
/**
|
|
53
|
+
* Release a hold, or abandon a payment nobody completed. A succeeded payment is refunded instead.
|
|
54
|
+
*
|
|
55
|
+
* Throws `409` with `isPaymentInProgress` while the customer may still be on the bank's page or
|
|
56
|
+
* in 3-D Secure: nothing was cancelled, the message says when to try again, and an unpaid
|
|
57
|
+
* payment is cancelled automatically anyway. The client does not retry it.
|
|
58
|
+
*/
|
|
59
|
+
cancel: (id, params = {}, options) => this.http.post(`/v1/payment_intents/${encodeURIComponent(id)}/cancel`, params, options),
|
|
60
|
+
};
|
|
61
|
+
checkoutSessions = {
|
|
62
|
+
create: (params, options) => this.http.post("/v1/checkout_sessions", params, options),
|
|
63
|
+
retrieve: (id) => this.http.get(`/v1/checkout_sessions/${encodeURIComponent(id)}`),
|
|
64
|
+
list: (params) => this.http.get("/v1/checkout_sessions", params),
|
|
65
|
+
expire: (id, options) => this.http.post(`/v1/checkout_sessions/${encodeURIComponent(id)}/expire`, {}, options),
|
|
66
|
+
};
|
|
67
|
+
refunds = {
|
|
68
|
+
/**
|
|
69
|
+
* Refund all or part of a payment.
|
|
70
|
+
*
|
|
71
|
+
* 🔴 `status: "pending"` means the refund was sent and the bank's answer has not reached us yet;
|
|
72
|
+
* the money may already be on its way back. Do not refund again — the pending amount is held
|
|
73
|
+
* against what is refundable — and wait for `refund.succeeded` or `refund.failed`. A thrown
|
|
74
|
+
* error with `isOutcomeUnknown` means the same without a refund object to hold: list the
|
|
75
|
+
* payment's refunds to see it, and never retry under a new idempotency key.
|
|
76
|
+
*/
|
|
77
|
+
create: (params, options) => this.http.post("/v1/refunds", params, options),
|
|
78
|
+
retrieve: (id) => this.http.get(`/v1/refunds/${encodeURIComponent(id)}`),
|
|
79
|
+
list: (params) => this.http.get("/v1/refunds", params),
|
|
80
|
+
};
|
|
81
|
+
customers = {
|
|
82
|
+
create: (params, options) => this.http.post("/v1/customers", params, options),
|
|
83
|
+
retrieve: (id) => this.http.get(`/v1/customers/${encodeURIComponent(id)}`),
|
|
84
|
+
list: (params) => this.http.get("/v1/customers", params),
|
|
85
|
+
/** The cards this customer has let you keep. */
|
|
86
|
+
listPaymentMethods: (id) => this.http.get(`/v1/customers/${encodeURIComponent(id)}/payment_methods`),
|
|
87
|
+
};
|
|
88
|
+
products = {
|
|
89
|
+
create: (params, options) => this.http.post("/v1/products", params, options),
|
|
90
|
+
retrieve: (id) => this.http.get(`/v1/products/${encodeURIComponent(id)}`),
|
|
91
|
+
list: (params) => this.http.get("/v1/products", params),
|
|
92
|
+
/** Everything is editable, including `active: false` to archive it. Prices are left alone. */
|
|
93
|
+
update: (id, params, options) => this.http.post(`/v1/products/${encodeURIComponent(id)}`, params, options),
|
|
94
|
+
};
|
|
95
|
+
prices = {
|
|
96
|
+
/**
|
|
97
|
+
* Give `unit_amount` in MINOR UNITS — 6000 is $60.00 — or a `custom_unit_amount` range for
|
|
98
|
+
* the payer to choose within. Exactly one of the two.
|
|
99
|
+
*/
|
|
100
|
+
create: (params, options) => this.http.post("/v1/prices", params, options),
|
|
101
|
+
retrieve: (id) => this.http.get(`/v1/prices/${encodeURIComponent(id)}`),
|
|
102
|
+
/** `product` is how you read one product's prices — they are not embedded in the product. */
|
|
103
|
+
list: (params) => this.http.get("/v1/prices", params),
|
|
104
|
+
/**
|
|
105
|
+
* 🔴 Only `active`, `nickname` and `metadata`. The amount cannot be changed: a payment link
|
|
106
|
+
* already stores this id, and repricing it would change what that link charges with nothing in
|
|
107
|
+
* the record to say so. Create a new price and archive this one.
|
|
108
|
+
*/
|
|
109
|
+
update: (id, params, options) => this.http.post(`/v1/prices/${encodeURIComponent(id)}`, params, options),
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* One URL that takes a payment, over and over.
|
|
113
|
+
*
|
|
114
|
+
* ```ts
|
|
115
|
+
* const link = await sp.paymentLinks.create({ price: price.id });
|
|
116
|
+
* // Share link.url — every visitor gets their own payment.
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
paymentLinks = {
|
|
120
|
+
/**
|
|
121
|
+
* The `price` must be an active price of yours. Its amount is what the link charges, and
|
|
122
|
+
* nothing a visitor sends can change it — the one exception is a price with a
|
|
123
|
+
* `custom_unit_amount`, where the payer chooses inside the range you published.
|
|
124
|
+
*/
|
|
125
|
+
create: (params, options) => this.http.post("/v1/payment_links", params, options),
|
|
126
|
+
retrieve: (id) => this.http.get(`/v1/payment_links/${encodeURIComponent(id)}`),
|
|
127
|
+
list: (params) => this.http.get("/v1/payment_links", params),
|
|
128
|
+
/**
|
|
129
|
+
* 🔴 Everything except the price and the quantity. Those are fixed for the life of the link:
|
|
130
|
+
* the URL is already on a poster, in a QR code and in messages nobody can recall, and the
|
|
131
|
+
* people who will pay it have been told what it costs. Create a new link and deactivate this
|
|
132
|
+
* one — `paymentLinks.update(id, { active: false })`.
|
|
133
|
+
*/
|
|
134
|
+
update: (id, params, options) => this.http.post(`/v1/payment_links/${encodeURIComponent(id)}`, params, options),
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* The rates you charge on invoice lines.
|
|
138
|
+
*
|
|
139
|
+
* Belize GST — 12.5%, added on top — is created for you when your account is. A rate's
|
|
140
|
+
* percentage and inclusivity cannot be changed: see the note on `update`.
|
|
141
|
+
*/
|
|
142
|
+
taxRates = {
|
|
143
|
+
create: (params, options) => this.http.post("/v1/tax_rates", params, options),
|
|
144
|
+
retrieve: (id) => this.http.get(`/v1/tax_rates/${encodeURIComponent(id)}`),
|
|
145
|
+
list: (params) => this.http.get("/v1/tax_rates", params),
|
|
146
|
+
/**
|
|
147
|
+
* 🔴 Only `display_name`, `active` and `metadata`. A finalized invoice names the rate that
|
|
148
|
+
* produced the tax printed on it, and a rate that moved would make the two disagree on a
|
|
149
|
+
* document your customer is already holding. Create a new rate and archive this one —
|
|
150
|
+
* `taxRates.update(id, { active: false })`.
|
|
151
|
+
*/
|
|
152
|
+
update: (id, params, options) => this.http.post(`/v1/tax_rates/${encodeURIComponent(id)}`, params, options),
|
|
153
|
+
};
|
|
154
|
+
/**
|
|
155
|
+
* Bill a customer, send them a link, and let us chase it.
|
|
156
|
+
*
|
|
157
|
+
* ```ts
|
|
158
|
+
* const invoice = await sp.invoices.create({ customer: cus.id, days_until_due: 14 });
|
|
159
|
+
* await sp.invoiceItems.create({ invoice: invoice.id, description: "Dive", unit_amount: 50000 });
|
|
160
|
+
* const open = await sp.invoices.finalize(invoice.id); // assigns INV-0007, freezes the totals
|
|
161
|
+
* await sp.invoices.send(invoice.id);
|
|
162
|
+
* ```
|
|
163
|
+
*
|
|
164
|
+
* 🔴 An invoice is created as a DRAFT and its money comes from its lines. Finalizing is the
|
|
165
|
+
* moment it becomes a document: it takes a number from your own consecutive sequence, freezes
|
|
166
|
+
* the totals, and stops the lines being touched. There is no way back from that — fix a mistake
|
|
167
|
+
* by voiding it and raising a new one.
|
|
168
|
+
*/
|
|
169
|
+
invoices = {
|
|
170
|
+
create: (params, options) => this.http.post("/v1/invoices", params, options),
|
|
171
|
+
retrieve: (id) => this.http.get(`/v1/invoices/${encodeURIComponent(id)}`),
|
|
172
|
+
list: (params) => this.http.get("/v1/invoices", params),
|
|
173
|
+
update: (id, params, options) => this.http.post(`/v1/invoices/${encodeURIComponent(id)}`, params, options),
|
|
174
|
+
/**
|
|
175
|
+
* Gives the invoice its number, freezes its totals, and moves it to `open`.
|
|
176
|
+
*
|
|
177
|
+
* ⚠️ Irreversible. Afterwards the lines and the amounts cannot change — by us, by you, or by
|
|
178
|
+
* anything with a database connection. It does not email anything; `send` does that.
|
|
179
|
+
*/
|
|
180
|
+
finalize: (id, options) => this.http.post(`/v1/invoices/${encodeURIComponent(id)}/finalize`, {}, options),
|
|
181
|
+
/**
|
|
182
|
+
* Emails the customer a link to the hosted page. Safe to call again.
|
|
183
|
+
*
|
|
184
|
+
* A success means the email left. Two failures say otherwise:
|
|
185
|
+
* - `503 email_not_sent`: it could not be sent right now, and nothing about the invoice changed.
|
|
186
|
+
* The client retries it for you under the same key (a 503 is never stored against a key, so a
|
|
187
|
+
* retry really sends); if it still fails, retry later with `{ idempotencyKey: err.idempotencyKey }`.
|
|
188
|
+
* - `400 email_recipient_rejected` (`param: "customer.email"`): the address itself was refused.
|
|
189
|
+
* Not retryable — fix the customer's email first.
|
|
190
|
+
*/
|
|
191
|
+
send: (id, options) => this.http.post(`/v1/invoices/${encodeURIComponent(id)}/send`, {}, options),
|
|
192
|
+
/**
|
|
193
|
+
* Marks an invoice as raised in error. It keeps its number and its lines, so the copy in the
|
|
194
|
+
* customer's inbox still matches something.
|
|
195
|
+
*
|
|
196
|
+
* 🔴 Refused once any money has been received. Refund the payment first, then void — in that
|
|
197
|
+
* order, so the money moves before the paperwork does.
|
|
198
|
+
*/
|
|
199
|
+
void: (id, options) => this.http.post(`/v1/invoices/${encodeURIComponent(id)}/void`, {}, options),
|
|
200
|
+
/** The bill was owed and is being written off. Not the same as void: the debt was real. */
|
|
201
|
+
markUncollectible: (id, options) => this.http.post(`/v1/invoices/${encodeURIComponent(id)}/mark_uncollectible`, {}, options),
|
|
202
|
+
/**
|
|
203
|
+
* Refunds the amount by which an invoice has been overpaid, from its most recent payment.
|
|
204
|
+
*
|
|
205
|
+
* An invoice can be overpaid: two browser tabs can both reach the hosted page and both be paid,
|
|
206
|
+
* and the money moved at the bank before anyone heard about it. `amount_due` goes negative and
|
|
207
|
+
* this is how you give the difference back.
|
|
208
|
+
*
|
|
209
|
+
* 🔴 `amount_paid` does NOT change — the payment happened, and the refund is its own record, so
|
|
210
|
+
* the excess is netted against anything already refunded. Calling this twice returns an error
|
|
211
|
+
* rather than refunding twice. Returns the refund, not the invoice.
|
|
212
|
+
*/
|
|
213
|
+
refundOverpayment: (id, options) => this.http.post(`/v1/invoices/${encodeURIComponent(id)}/refund_overpayment`, {}, options),
|
|
214
|
+
/**
|
|
215
|
+
* Charge a card the customer already saved with you, with nobody present.
|
|
216
|
+
*
|
|
217
|
+
* ⚠️ **Beta.** This relies on a `tii` parameter Belize Bank requires and documents nowhere, and
|
|
218
|
+
* a merchant-initiated charge has never been proved in production. There is no way to charge a
|
|
219
|
+
* card the customer has not saved — `send` the invoice and let them pay on the hosted page.
|
|
220
|
+
*
|
|
221
|
+
* 🔴 A `202` means the charge was sent and the bank's answer has not reached us yet. Tell it from
|
|
222
|
+
* a `200` by the status, not by the body — the invoice looks the same either way:
|
|
223
|
+
*
|
|
224
|
+
* ```ts
|
|
225
|
+
* const invoice = await sp.invoices.pay(id, { payment_method: "pm_..." });
|
|
226
|
+
* if (invoice.lastResponse.statusCode === 202) {
|
|
227
|
+
* // sent, outcome unknown: the card may already have been charged
|
|
228
|
+
* }
|
|
229
|
+
* ```
|
|
230
|
+
*
|
|
231
|
+
* Do NOT call this again under a new idempotency key. `paymentIntents.list({ invoice: id })`
|
|
232
|
+
* shows the payment `processing`; it settles on its own, and `invoice.paid` or
|
|
233
|
+
* `invoice.payment_succeeded` follows if it went through. A `500` with `isOutcomeUnknown` means
|
|
234
|
+
* the same thing, said by an error.
|
|
235
|
+
*/
|
|
236
|
+
pay: (id, params, options) => this.http.post(`/v1/invoices/${encodeURIComponent(id)}/pay`, params, options),
|
|
237
|
+
/**
|
|
238
|
+
* 🔴 VOIDS a draft; it does not delete it, and the returned invoice says
|
|
239
|
+
* `status: "void"`. Stripe deletes drafts and this method exists so code written against that
|
|
240
|
+
* does not break, but a bill that was raised stays on the record here — "was this customer
|
|
241
|
+
* ever invoiced?" has to remain answerable.
|
|
242
|
+
*/
|
|
243
|
+
del: (id, options) => this.http.request("DELETE", `/v1/invoices/${encodeURIComponent(id)}`, undefined, options),
|
|
244
|
+
};
|
|
245
|
+
/**
|
|
246
|
+
* The lines on a draft invoice.
|
|
247
|
+
*
|
|
248
|
+
* 🔴 Every write is refused once the invoice has been finalized. Freezing the totals alone would
|
|
249
|
+
* not be enough: a line edited afterwards leaves an invoice whose stored total is right and whose
|
|
250
|
+
* printed lines do not add up to it, which is worse than being simply wrong.
|
|
251
|
+
*/
|
|
252
|
+
invoiceItems = {
|
|
253
|
+
/** Give `price` to quote your catalogue, or `unit_amount` (minor units) and `description`. */
|
|
254
|
+
create: (params, options) => this.http.post("/v1/invoice_items", params, options),
|
|
255
|
+
list: (params) => this.http.get("/v1/invoice_items", params),
|
|
256
|
+
update: (id, params, options) => this.http.post(`/v1/invoice_items/${encodeURIComponent(id)}`, params, options),
|
|
257
|
+
/** The one thing in invoicing that really deletes, and only while the invoice is a draft. */
|
|
258
|
+
del: (id, options) => this.http.request("DELETE", `/v1/invoice_items/${encodeURIComponent(id)}`, undefined, options),
|
|
259
|
+
};
|
|
260
|
+
paymentMethods = {
|
|
261
|
+
/**
|
|
262
|
+
* Forget a saved card.
|
|
263
|
+
*
|
|
264
|
+
* ⚠️ Definitive here, best effort at the bank. If the gateway refuses to forget the binding the
|
|
265
|
+
* card is still detached on our side, which is what governs whether it can ever be charged
|
|
266
|
+
* again — so this succeeding means what you need it to mean.
|
|
267
|
+
*/
|
|
268
|
+
detach: (id, options) => this.http.post(`/v1/payment_methods/${encodeURIComponent(id)}/detach`, {}, options),
|
|
269
|
+
};
|
|
270
|
+
balance = {
|
|
271
|
+
retrieve: () => this.http.get("/v1/balance"),
|
|
272
|
+
};
|
|
273
|
+
/**
|
|
274
|
+
* Every movement of your balance, in one list.
|
|
275
|
+
*
|
|
276
|
+
* `net` sums to `balance.pending + balance.available`, so this is the list you reconcile with.
|
|
277
|
+
* A charge appears once and changes `status` from `pending` to `available`; the settlement
|
|
278
|
+
* release is not a second row.
|
|
279
|
+
*/
|
|
280
|
+
balanceTransactions = {
|
|
281
|
+
list: (params) => this.http.get("/v1/balance_transactions", params),
|
|
282
|
+
};
|
|
283
|
+
/**
|
|
284
|
+
* Payouts: money on its way to your bank account.
|
|
285
|
+
*
|
|
286
|
+
* 🔴 Read-only, and that is the design. A payout's amount is never a number you supply — it is
|
|
287
|
+
* the whole of your available balance at the moment of the cut, decided by the ledger. Payouts
|
|
288
|
+
* are cut by the schedule you chose in the dashboard, or by asking for one there.
|
|
289
|
+
*
|
|
290
|
+
* ⚠️ Between `pending` and `paid` there is a person making a bank transfer, because the acquiring
|
|
291
|
+
* bank offers no disbursement API. `bank_reference` is the bank's own reference for it.
|
|
292
|
+
*/
|
|
293
|
+
payouts = {
|
|
294
|
+
retrieve: (id) => this.http.get(`/v1/payouts/${encodeURIComponent(id)}`),
|
|
295
|
+
list: (params) => this.http.get("/v1/payouts", params),
|
|
296
|
+
/** Everything one payout covers, as `balance_transaction`s whose `net` sums to its amount. */
|
|
297
|
+
listTransactions: (id, params) => this.http.get("/v1/balance_transactions", {
|
|
298
|
+
...params,
|
|
299
|
+
payout: id,
|
|
300
|
+
}),
|
|
301
|
+
};
|
|
302
|
+
/**
|
|
303
|
+
* Reports: the documents you reconcile your own books against.
|
|
304
|
+
*
|
|
305
|
+
* ```ts
|
|
306
|
+
* const month = await sp.reports.balanceSummary();
|
|
307
|
+
* if (!month.reconciled) throw new Error("our statement and our balance disagree");
|
|
308
|
+
*
|
|
309
|
+
* const csv = await sp.reports.export("payments", { start: "2026-09-01", end: "2026-10-01" });
|
|
310
|
+
* await pipeline(Readable.fromWeb(csv.body!), createWriteStream("payments.csv"));
|
|
311
|
+
* ```
|
|
312
|
+
*/
|
|
313
|
+
reports = {
|
|
314
|
+
/**
|
|
315
|
+
* Opening balance, everything that moved, closing balance — and whether the ledger agrees.
|
|
316
|
+
*
|
|
317
|
+
* 🔴 Check `reconciled`. It is false only when the period's movements and the ledger's own
|
|
318
|
+
* recomputed balance disagree, which means your statement and your balance are telling you
|
|
319
|
+
* different things. Get in touch rather than reconciling around it.
|
|
320
|
+
*
|
|
321
|
+
* Defaults to the current calendar month up to now. At most 366 days per call.
|
|
322
|
+
*/
|
|
323
|
+
balanceSummary: (params) => this.http.get("/v1/reports/balance_summary", params),
|
|
324
|
+
/**
|
|
325
|
+
* A CSV export, as a STREAM.
|
|
326
|
+
*
|
|
327
|
+
* Returns the `Response` rather than a string: a year of payments is tens of thousands of rows
|
|
328
|
+
* and reading it into memory to hand you a string is the thing this endpoint exists to avoid.
|
|
329
|
+
* Pipe `response.body`, or `await response.text()` when you know it is small.
|
|
330
|
+
*
|
|
331
|
+
* Every money column appears twice — `…_minor` is the integer this API speaks in, and the
|
|
332
|
+
* column beside it is the same figure formatted for a person. Dates are ISO 8601 UTC. At most
|
|
333
|
+
* 366 days per call; page by date for longer.
|
|
334
|
+
*/
|
|
335
|
+
export: (kind, params) => this.http.raw(`/v1/reports/exports/${kind}.csv`, params),
|
|
336
|
+
/**
|
|
337
|
+
* Everything one payout paid for, as CSV. Its lines sum to the payout's amount by
|
|
338
|
+
* construction, and the header prints that amount separately so the two can be compared.
|
|
339
|
+
*/
|
|
340
|
+
payoutReconciliation: (payoutId) => this.http.raw(`/v1/reports/payout_reconciliation/${encodeURIComponent(payoutId)}`),
|
|
341
|
+
};
|
|
342
|
+
/**
|
|
343
|
+
* Your own account.
|
|
344
|
+
*
|
|
345
|
+
* ```ts
|
|
346
|
+
* const account = await sp.account.retrieve();
|
|
347
|
+
* if (!account.live_enabled) {
|
|
348
|
+
* // Still in test. account.requirements.currently_due says what is missing.
|
|
349
|
+
* }
|
|
350
|
+
* ```
|
|
351
|
+
*
|
|
352
|
+
* 🔴 THERE IS NO `account.application` OR `files` RESOURCE HERE, and that is deliberate rather
|
|
353
|
+
* than unfinished. Those endpoints exist — `/v1/account/application`, `/v1/files` — and they are
|
|
354
|
+
* SESSION-ONLY: they carry the beneficial owners' dates of birth, passport numbers and home
|
|
355
|
+
* addresses, and the bank account the business will be paid into. An API key is a long-lived
|
|
356
|
+
* bearer credential in an environment variable on somebody's server; a leaked one should be a
|
|
357
|
+
* payments incident, not an identity-theft one.
|
|
358
|
+
*
|
|
359
|
+
* So an SDK method for them would be a method that returns 403 every time it is called, for every
|
|
360
|
+
* caller, forever — which is worse than no method, because a client library is read as a
|
|
361
|
+
* statement of what the API can do. The application is filled in from the dashboard. If a
|
|
362
|
+
* programmatic path is ever needed it will be a purpose-scoped credential, not this one.
|
|
363
|
+
*/
|
|
364
|
+
account = {
|
|
365
|
+
retrieve: () => this.http.get("/v1/account"),
|
|
366
|
+
};
|
|
367
|
+
events = {
|
|
368
|
+
retrieve: (id) => this.http.get(`/v1/events/${encodeURIComponent(id)}`),
|
|
369
|
+
/** Everything that has happened, newest first. Use it to catch up after a delivery failure. */
|
|
370
|
+
list: (params) => this.http.get("/v1/events", params),
|
|
371
|
+
};
|
|
372
|
+
webhookEndpoints = {
|
|
373
|
+
create: (params, options) => this.http.post("/v1/webhook_endpoints", params, options),
|
|
374
|
+
list: (params) => this.http.get("/v1/webhook_endpoints", params),
|
|
375
|
+
/** Issues a new secret; the old one keeps working for the overlap you specify. */
|
|
376
|
+
rotateSecret: (id, params = {}, options) => this.http.post(`/v1/webhook_endpoints/${encodeURIComponent(id)}/rotate_secret`, params, options),
|
|
377
|
+
retrieve: (id) => this.http.get(`/v1/webhook_endpoints/${encodeURIComponent(id)}`),
|
|
378
|
+
/**
|
|
379
|
+
* Change the URL, the description or which events are sent, or switch the endpoint off and on.
|
|
380
|
+
*
|
|
381
|
+
* The signing secret is deliberately unchanged: moving an endpoint to a new hostname and having
|
|
382
|
+
* its key rotate underneath is how a migration becomes an outage. Rotate explicitly.
|
|
383
|
+
*/
|
|
384
|
+
update: (id, params, options) => this.http.post(`/v1/webhook_endpoints/${encodeURIComponent(id)}`, params, options),
|
|
385
|
+
/** Every attempt at every delivery for this endpoint, newest first. */
|
|
386
|
+
deliveries: (id, params) => this.http.get(`/v1/webhook_endpoints/${encodeURIComponent(id)}/deliveries`, params),
|
|
387
|
+
/** Deliver a `webhook_endpoint.test` event to THIS endpoint and no other. */
|
|
388
|
+
sendTest: (id, options) => this.http.post(`/v1/webhook_endpoints/${encodeURIComponent(id)}/send_test`, {}, options),
|
|
389
|
+
/** Disabled, never deleted: the delivery history is how you answer "did you send it?" later. */
|
|
390
|
+
disable: (id) => this.http.delete(`/v1/webhook_endpoints/${encodeURIComponent(id)}`),
|
|
391
|
+
/**
|
|
392
|
+
* Send one delivery again, now, without waiting for its schedule.
|
|
393
|
+
*
|
|
394
|
+
* ⚠️ The delivery keeps its own retry schedule either way. This is for after you have fixed the
|
|
395
|
+
* endpoint and do not want to wait out the backoff.
|
|
396
|
+
*/
|
|
397
|
+
retryDelivery: (deliveryId, options) => this.http.post(`/v1/webhook_deliveries/${encodeURIComponent(deliveryId)}/retry`, {}, options),
|
|
398
|
+
};
|
|
399
|
+
/**
|
|
400
|
+
* Risk rules: the tests every one of your payments passes through before the bank is called.
|
|
401
|
+
*
|
|
402
|
+
* ```ts
|
|
403
|
+
* await sp.riskRules.create({
|
|
404
|
+
* name: "Review anything over BZ$2,000",
|
|
405
|
+
* action: "review",
|
|
406
|
+
* predicate: { all: [{ attr: "amount_minor", op: "gte", value: 200000 }] },
|
|
407
|
+
* });
|
|
408
|
+
* ```
|
|
409
|
+
*
|
|
410
|
+
* The list includes the platform defaults Secure Processing applies to every business. Those
|
|
411
|
+
* carry `platform_default: true`, cannot be edited, and are overridden only by an entry on your
|
|
412
|
+
* own allow list.
|
|
413
|
+
*/
|
|
414
|
+
riskRules = {
|
|
415
|
+
create: (params, options) => this.http.post("/v1/risk_rules", params, options),
|
|
416
|
+
retrieve: (id) => this.http.get(`/v1/risk_rules/${encodeURIComponent(id)}`),
|
|
417
|
+
list: (params) => this.http.get("/v1/risk_rules", params),
|
|
418
|
+
/**
|
|
419
|
+
* 🔴 `name`, `enabled`, `priority` and `metadata` only. Every evaluation records the rule that
|
|
420
|
+
* fired, so a rewritten predicate would make those records describe a test that no longer
|
|
421
|
+
* exists. Create a new rule and disable this one.
|
|
422
|
+
*/
|
|
423
|
+
update: (id, params, options) => this.http.post(`/v1/risk_rules/${encodeURIComponent(id)}`, params, options),
|
|
424
|
+
};
|
|
425
|
+
/**
|
|
426
|
+
* Allow and block lists: exact values, checked before any rule runs.
|
|
427
|
+
*
|
|
428
|
+
* An `allow` entry beats everything, including our platform defaults. A `block` entry refuses
|
|
429
|
+
* the payment with `payment_blocked`.
|
|
430
|
+
*
|
|
431
|
+
* ⚠️ `card_fingerprint` can only match on a charge against a card already saved. The card number
|
|
432
|
+
* is entered on the bank's own page, so there is nothing to fingerprint on somebody's first
|
|
433
|
+
* visit to a checkout.
|
|
434
|
+
*/
|
|
435
|
+
riskLists = {
|
|
436
|
+
create: (params, options) => this.http.post("/v1/risk_lists", params, options),
|
|
437
|
+
retrieve: (id) => this.http.get(`/v1/risk_lists/${encodeURIComponent(id)}`),
|
|
438
|
+
list: (params) => this.http.get("/v1/risk_lists", params),
|
|
439
|
+
/**
|
|
440
|
+
* ⚠️ A real delete, unlike `webhookEndpoints.disable`. A list is a live control surface:
|
|
441
|
+
* unblocking somebody has to actually unblock them. What the entry did is kept in the
|
|
442
|
+
* evaluations it decided.
|
|
443
|
+
*/
|
|
444
|
+
remove: (id) => this.http.delete(`/v1/risk_lists/${encodeURIComponent(id)}`),
|
|
445
|
+
};
|
|
446
|
+
/**
|
|
447
|
+
* Reviews: payments a rule asked you to look at.
|
|
448
|
+
*
|
|
449
|
+
* A review exists only for a payment that was credited — an abandoned checkout never makes one.
|
|
450
|
+
* Close it by approving (keep the money) or refunding (give it all back); the refund goes
|
|
451
|
+
* through the ordinary refund path, fee proration and all.
|
|
452
|
+
*/
|
|
453
|
+
reviews = {
|
|
454
|
+
retrieve: (id) => this.http.get(`/v1/reviews/${encodeURIComponent(id)}`),
|
|
455
|
+
list: (params) => this.http.get("/v1/reviews", params),
|
|
456
|
+
approve: (id, params = {}, options) => this.http.post(`/v1/reviews/${encodeURIComponent(id)}/approve`, params, options),
|
|
457
|
+
/**
|
|
458
|
+
* Refunds the payment in full and closes the review. The bank refusing leaves it open.
|
|
459
|
+
*
|
|
460
|
+
* 🔴 A `202` means the refund was sent and the bank has not confirmed it: the refund is
|
|
461
|
+
* `pending`, and the review stays `open` until it settles. Check the status, not only the body:
|
|
462
|
+
*
|
|
463
|
+
* ```ts
|
|
464
|
+
* const review = await sp.reviews.refund(id);
|
|
465
|
+
* if (review.lastResponse.statusCode === 202) {
|
|
466
|
+
* // refund pending; the review closes once it settles
|
|
467
|
+
* }
|
|
468
|
+
* ```
|
|
469
|
+
*
|
|
470
|
+
* Calling it again never refunds twice: while the refund is pending it answers `202` again, and
|
|
471
|
+
* once it has settled it closes the review as `refunded` (`200`).
|
|
472
|
+
*/
|
|
473
|
+
refund: (id, params = {}, options) => this.http.post(`/v1/reviews/${encodeURIComponent(id)}/refund`, params, options),
|
|
474
|
+
};
|
|
475
|
+
/**
|
|
476
|
+
* Disputes: a chargeback, and the evidence you answer it with.
|
|
477
|
+
*
|
|
478
|
+
* ```ts
|
|
479
|
+
* const dispute = await sp.disputes.retrieve("dp_...");
|
|
480
|
+
* await sp.disputes.update(dispute.id, {
|
|
481
|
+
* evidence: { shipping_carrier: "DHL", shipping_tracking_number: "CP123456789BZ" },
|
|
482
|
+
* submit: true,
|
|
483
|
+
* });
|
|
484
|
+
* ```
|
|
485
|
+
*
|
|
486
|
+
* ⚠️ The file fields take a `file_` id, and uploading one is **session only** — there is no
|
|
487
|
+
* `files` resource on this client. A document uploaded for a dispute is often an invoice naming
|
|
488
|
+
* a customer or a signed delivery note, and an API key is a long-lived credential sitting in an
|
|
489
|
+
* environment variable. Upload from the dashboard and paste the id, or send only the text fields.
|
|
490
|
+
*
|
|
491
|
+
* 🔴 THERE IS NO `create`. A dispute is something the card network asserted, not something a
|
|
492
|
+
* business declares about itself — Belize Bank emails us a case reference and we open it. You
|
|
493
|
+
* hear about it through `dispute.created` and an email to the account owner.
|
|
494
|
+
*
|
|
495
|
+
* ⚠️ `evidence_details.due_by` is the only field worth alerting on. A dispute is lost by silence
|
|
496
|
+
* far more often than by argument, and the bank does not extend the deadline.
|
|
497
|
+
*/
|
|
498
|
+
disputes = {
|
|
499
|
+
retrieve: (id) => this.http.get(`/v1/disputes/${encodeURIComponent(id)}`),
|
|
500
|
+
list: (params) => this.http.get("/v1/disputes", params),
|
|
501
|
+
/**
|
|
502
|
+
* Add evidence, and optionally submit it.
|
|
503
|
+
*
|
|
504
|
+
* Fields MERGE, so posting the shipping section does not erase the receipt. Submitting is
|
|
505
|
+
* final: we forward the bundle to Belize Bank by hand, and the bundle they see has to be the
|
|
506
|
+
* one this record shows.
|
|
507
|
+
*/
|
|
508
|
+
update: (id, params, options) => this.http.post(`/v1/disputes/${encodeURIComponent(id)}`, params, options),
|
|
509
|
+
/**
|
|
510
|
+
* Concede. Stripe's verb and Stripe's meaning: you are not contesting this one.
|
|
511
|
+
*
|
|
512
|
+
* ⚠️ It moves no money and does not mark the dispute lost. The bank has not answered yet, and
|
|
513
|
+
* we will not debit you before the network has decided.
|
|
514
|
+
*/
|
|
515
|
+
close: (id, options) => this.http.post(`/v1/disputes/${encodeURIComponent(id)}/close`, {}, options),
|
|
516
|
+
};
|
|
517
|
+
/** Verify an incoming webhook and get the event. Pass the RAW body. */
|
|
518
|
+
webhooks = { constructEvent };
|
|
519
|
+
}
|
|
520
|
+
export default SecureProcessing;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ListParams, ListResponse } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Every item from a list endpoint, fetching the next page only when you reach it.
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* let total = 0;
|
|
7
|
+
* for await (const line of autoPaginate((page) => sp.payouts.listTransactions(payoutId, page))) {
|
|
8
|
+
* total += line.net;
|
|
9
|
+
* }
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* 🔴 Why it exists: the balance-and-payouts page reconciled a payout by summing
|
|
13
|
+
* `listTransactions(id).data` — the FIRST page, ten rows by default — and claimed the result
|
|
14
|
+
* equals the payout's amount. For any payout covering more than ten movements it did not, and the
|
|
15
|
+
* snippet's whole point is that a mismatch means something is wrong. A total has to read every page.
|
|
16
|
+
*
|
|
17
|
+
* Pass a function that makes one list call with the paging parameters it is given; any filters go
|
|
18
|
+
* in that call. Pages are requested 100 at a time.
|
|
19
|
+
*
|
|
20
|
+
* It never loops for ever, and it never silently stops short:
|
|
21
|
+
* - an empty page ends the walk, whatever `has_more` says;
|
|
22
|
+
* - a page that repeats an id already seen THROWS before yielding it. That is what happens when the
|
|
23
|
+
* function drops its `page` argument (`() => sp.payouts.list({...})`), or when the endpoint takes
|
|
24
|
+
* no cursor but still says `has_more` (`riskRules.list` and `webhookEndpoints.deliveries` page
|
|
25
|
+
* nothing). Stopping quietly there would hand a total over the first page as if it were all of
|
|
26
|
+
* them — the very bug this exists to prevent — so it is an error with the reason in it.
|
|
27
|
+
*/
|
|
28
|
+
export declare function autoPaginate<T extends {
|
|
29
|
+
id: string;
|
|
30
|
+
}>(listPage: (page: ListParams) => Promise<ListResponse<T>>, options?: {
|
|
31
|
+
limit?: number;
|
|
32
|
+
}): AsyncGenerator<T, void, undefined>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// @ts-self-types="./pagination.d.ts"
|
|
2
|
+
/**
|
|
3
|
+
* Every item from a list endpoint, fetching the next page only when you reach it.
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* let total = 0;
|
|
7
|
+
* for await (const line of autoPaginate((page) => sp.payouts.listTransactions(payoutId, page))) {
|
|
8
|
+
* total += line.net;
|
|
9
|
+
* }
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* 🔴 Why it exists: the balance-and-payouts page reconciled a payout by summing
|
|
13
|
+
* `listTransactions(id).data` — the FIRST page, ten rows by default — and claimed the result
|
|
14
|
+
* equals the payout's amount. For any payout covering more than ten movements it did not, and the
|
|
15
|
+
* snippet's whole point is that a mismatch means something is wrong. A total has to read every page.
|
|
16
|
+
*
|
|
17
|
+
* Pass a function that makes one list call with the paging parameters it is given; any filters go
|
|
18
|
+
* in that call. Pages are requested 100 at a time.
|
|
19
|
+
*
|
|
20
|
+
* It never loops for ever, and it never silently stops short:
|
|
21
|
+
* - an empty page ends the walk, whatever `has_more` says;
|
|
22
|
+
* - a page that repeats an id already seen THROWS before yielding it. That is what happens when the
|
|
23
|
+
* function drops its `page` argument (`() => sp.payouts.list({...})`), or when the endpoint takes
|
|
24
|
+
* no cursor but still says `has_more` (`riskRules.list` and `webhookEndpoints.deliveries` page
|
|
25
|
+
* nothing). Stopping quietly there would hand a total over the first page as if it were all of
|
|
26
|
+
* them — the very bug this exists to prevent — so it is an error with the reason in it.
|
|
27
|
+
*/
|
|
28
|
+
export async function* autoPaginate(listPage, options = {}) {
|
|
29
|
+
const seen = new Set();
|
|
30
|
+
let startingAfter;
|
|
31
|
+
for (;;) {
|
|
32
|
+
const page = await listPage({ limit: options.limit ?? 100, starting_after: startingAfter });
|
|
33
|
+
const last = page.data[page.data.length - 1];
|
|
34
|
+
if (startingAfter !== undefined && last?.id === startingAfter) {
|
|
35
|
+
throw new Error(`autoPaginate: the list did not advance past ${startingAfter}: does listPage pass its page ` +
|
|
36
|
+
"argument through, and does this endpoint take a starting_after cursor?");
|
|
37
|
+
}
|
|
38
|
+
const repeated = page.data.find((item) => seen.has(item.id));
|
|
39
|
+
if (repeated) {
|
|
40
|
+
throw new Error(`autoPaginate: ${repeated.id} was already returned by an earlier page, so the list did not ` +
|
|
41
|
+
"advance: does listPage pass its page argument through, and does this endpoint take a " +
|
|
42
|
+
"starting_after cursor?");
|
|
43
|
+
}
|
|
44
|
+
for (const item of page.data) {
|
|
45
|
+
seen.add(item.id);
|
|
46
|
+
yield item;
|
|
47
|
+
}
|
|
48
|
+
if (!page.has_more || !last)
|
|
49
|
+
return;
|
|
50
|
+
startingAfter = last.id;
|
|
51
|
+
}
|
|
52
|
+
}
|