@usebillow/sdk 0.5.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/CHANGELOG.md +9 -0
- package/LICENSE +21 -0
- package/README.md +274 -0
- package/dist/billing-C4RIMgH_.d.ts +1053 -0
- package/dist/billing-DZ4rIyg7.d.cts +1053 -0
- package/dist/billing-status-BZQN_gm7.d.cts +29 -0
- package/dist/billing-status-BZQN_gm7.d.ts +29 -0
- package/dist/chunk-CCG4F5FK.js +48 -0
- package/dist/chunk-CCG4F5FK.js.map +1 -0
- package/dist/chunk-Z6VXPONT.js +1493 -0
- package/dist/chunk-Z6VXPONT.js.map +1 -0
- package/dist/config.cjs +233 -0
- package/dist/config.cjs.map +1 -0
- package/dist/config.d.cts +104 -0
- package/dist/config.d.ts +104 -0
- package/dist/config.js +228 -0
- package/dist/config.js.map +1 -0
- package/dist/credits-C3Fe3TO0.d.cts +315 -0
- package/dist/credits-C3Fe3TO0.d.ts +315 -0
- package/dist/index.cjs +1560 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +2379 -0
- package/dist/index.d.ts +2379 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/ingestion.cjs +259 -0
- package/dist/ingestion.cjs.map +1 -0
- package/dist/ingestion.d.cts +182 -0
- package/dist/ingestion.d.ts +182 -0
- package/dist/ingestion.js +252 -0
- package/dist/ingestion.js.map +1 -0
- package/dist/react.cjs +360 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +71 -0
- package/dist/react.d.ts +71 -0
- package/dist/react.js +153 -0
- package/dist/react.js.map +1 -0
- package/dist/server.cjs +98 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +54 -0
- package/dist/server.d.ts +54 -0
- package/dist/server.js +96 -0
- package/dist/server.js.map +1 -0
- package/dist/status.cjs +60 -0
- package/dist/status.cjs.map +1 -0
- package/dist/status.d.cts +31 -0
- package/dist/status.d.ts +31 -0
- package/dist/status.js +3 -0
- package/dist/status.js.map +1 -0
- package/dist/webhooks.cjs +157 -0
- package/dist/webhooks.cjs.map +1 -0
- package/dist/webhooks.d.cts +391 -0
- package/dist/webhooks.d.ts +391 -0
- package/dist/webhooks.js +143 -0
- package/dist/webhooks.js.map +1 -0
- package/package.json +169 -0
|
@@ -0,0 +1,1493 @@
|
|
|
1
|
+
// src/client.ts
|
|
2
|
+
var REQUEST_ID_HEADER = "x-request-id";
|
|
3
|
+
function toQuery(params) {
|
|
4
|
+
const q = new URLSearchParams();
|
|
5
|
+
for (const [k, v] of Object.entries(params)) {
|
|
6
|
+
if (v !== void 0 && v !== null && v !== "") q.set(k, String(v));
|
|
7
|
+
}
|
|
8
|
+
const s = q.toString();
|
|
9
|
+
return s ? `?${s}` : "";
|
|
10
|
+
}
|
|
11
|
+
var BillowApiError = class extends Error {
|
|
12
|
+
status;
|
|
13
|
+
/**
|
|
14
|
+
* A known {@link BillowErrorCode} (widened to `string` so a newer server's code still type-checks),
|
|
15
|
+
* or one the SDK raises itself: `invalid_response` for a success (2xx other than 204) whose body
|
|
16
|
+
* was empty or could not be read or parsed. The request reached billow and was answered, but what
|
|
17
|
+
* it answered is unknown - for a write, treat the outcome as UNKNOWN (retry it under the same
|
|
18
|
+
* idempotency key), never as done or as refused.
|
|
19
|
+
*/
|
|
20
|
+
code;
|
|
21
|
+
/** Structured error context from the server (e.g. per-field validation issues), when present. */
|
|
22
|
+
details;
|
|
23
|
+
/** billow's per-request id (`x-request-id`) — quote it in a bug report to trace the server log. */
|
|
24
|
+
requestId;
|
|
25
|
+
constructor(status, code, message, details, requestId) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = "BillowApiError";
|
|
28
|
+
this.status = status;
|
|
29
|
+
this.code = code;
|
|
30
|
+
this.details = details;
|
|
31
|
+
this.requestId = requestId;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
var DEFAULT_TIMEOUT_MS = 6e4;
|
|
35
|
+
var DEFAULT_RETRY_BACKOFF_MS = 500;
|
|
36
|
+
function makeContext(bearer, opts) {
|
|
37
|
+
return {
|
|
38
|
+
fetch: opts.fetch ?? fetch,
|
|
39
|
+
baseUrl: resolveBaseUrl(opts.baseUrl),
|
|
40
|
+
bearer,
|
|
41
|
+
timeoutMs: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
42
|
+
maxRetries: opts.maxRetries ?? 0,
|
|
43
|
+
retryBackoffMs: opts.retryBackoffMs ?? DEFAULT_RETRY_BACKOFF_MS
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function isRetryable(status, safeToRepeat2) {
|
|
47
|
+
if (status === 429) return true;
|
|
48
|
+
if (status === null) return safeToRepeat2;
|
|
49
|
+
return safeToRepeat2 && status >= 500 && status <= 599;
|
|
50
|
+
}
|
|
51
|
+
function backoffMs(base, attempt, retryAfter) {
|
|
52
|
+
if (retryAfter) {
|
|
53
|
+
const secs = Number(retryAfter);
|
|
54
|
+
if (Number.isFinite(secs) && secs >= 0) return secs * 1e3;
|
|
55
|
+
}
|
|
56
|
+
return base * 2 ** attempt;
|
|
57
|
+
}
|
|
58
|
+
function abortReason(signal) {
|
|
59
|
+
return signal.reason ?? new DOMException("The operation was aborted.", "AbortError");
|
|
60
|
+
}
|
|
61
|
+
function sleep(ms, signal) {
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
if (signal?.aborted) return reject(abortReason(signal));
|
|
64
|
+
const timer = setTimeout(resolve, ms);
|
|
65
|
+
signal?.addEventListener(
|
|
66
|
+
"abort",
|
|
67
|
+
() => {
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
reject(abortReason(signal));
|
|
70
|
+
},
|
|
71
|
+
{ once: true }
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
async function sendWithResilience(ctx, url, init, opts) {
|
|
76
|
+
const { timeoutMs, safeToRepeat: safeToRepeat2, callerSignal } = opts;
|
|
77
|
+
if (callerSignal?.aborted) throw abortReason(callerSignal);
|
|
78
|
+
for (let attempt = 0; ; attempt++) {
|
|
79
|
+
const controller = new AbortController();
|
|
80
|
+
let timedOut = false;
|
|
81
|
+
const onCallerAbort = () => controller.abort(abortReason(callerSignal));
|
|
82
|
+
callerSignal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
83
|
+
const timer = timeoutMs > 0 ? setTimeout(() => {
|
|
84
|
+
timedOut = true;
|
|
85
|
+
controller.abort();
|
|
86
|
+
}, timeoutMs) : void 0;
|
|
87
|
+
let res = null;
|
|
88
|
+
let err;
|
|
89
|
+
try {
|
|
90
|
+
res = await ctx.fetch(url, { ...init, signal: controller.signal });
|
|
91
|
+
} catch (e) {
|
|
92
|
+
err = e;
|
|
93
|
+
} finally {
|
|
94
|
+
if (timer) clearTimeout(timer);
|
|
95
|
+
callerSignal?.removeEventListener("abort", onCallerAbort);
|
|
96
|
+
}
|
|
97
|
+
if (callerSignal?.aborted) throw abortReason(callerSignal);
|
|
98
|
+
if (res?.ok) return res;
|
|
99
|
+
const status = res ? res.status : null;
|
|
100
|
+
if (attempt < ctx.maxRetries && isRetryable(status, safeToRepeat2)) {
|
|
101
|
+
await sleep(
|
|
102
|
+
backoffMs(ctx.retryBackoffMs, attempt, res?.headers.get("retry-after") ?? null),
|
|
103
|
+
callerSignal
|
|
104
|
+
);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (res) return res;
|
|
108
|
+
if (timedOut) throw new Error(`billow request timed out after ${timeoutMs}ms`);
|
|
109
|
+
throw err instanceof Error ? err : new Error("billow request failed");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function safeToRepeat(method, hasIdempotencyKey) {
|
|
113
|
+
return method === "GET" || method === "HEAD" || hasIdempotencyKey;
|
|
114
|
+
}
|
|
115
|
+
var DEFAULT_BASE_URL = "https://api.usebillow.com";
|
|
116
|
+
function resolveBaseUrl(explicit) {
|
|
117
|
+
const fromEnv = typeof process !== "undefined" ? process.env?.["BILLOW_URL"] : void 0;
|
|
118
|
+
return (explicit || fromEnv || DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
119
|
+
}
|
|
120
|
+
async function apiRequest(ctx, method, path, body, options) {
|
|
121
|
+
return (await apiRequestWithStatus(ctx, method, path, body, options)).data;
|
|
122
|
+
}
|
|
123
|
+
async function apiRequestWithStatus(ctx, method, path, body, options) {
|
|
124
|
+
const init = {
|
|
125
|
+
method,
|
|
126
|
+
headers: {
|
|
127
|
+
authorization: `Bearer ${ctx.bearer}`,
|
|
128
|
+
...body ? { "content-type": "application/json" } : {},
|
|
129
|
+
...options?.idempotencyKey ? { "idempotency-key": options.idempotencyKey } : {}
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
if (body) init.body = JSON.stringify(body);
|
|
133
|
+
const res = await sendWithResilience(ctx, `${ctx.baseUrl}${path}`, init, {
|
|
134
|
+
timeoutMs: options?.timeoutMs ?? ctx.timeoutMs,
|
|
135
|
+
safeToRepeat: safeToRepeat(method, !!options?.idempotencyKey),
|
|
136
|
+
callerSignal: options?.signal
|
|
137
|
+
});
|
|
138
|
+
const text = await res.text().catch(() => null);
|
|
139
|
+
if (!res.ok) throw errorFrom(res, safeParse(text ?? ""));
|
|
140
|
+
return { status: res.status, data: parseSuccessBody(res, text) };
|
|
141
|
+
}
|
|
142
|
+
function requestIdOf(res) {
|
|
143
|
+
return res.headers.get(REQUEST_ID_HEADER) ?? void 0;
|
|
144
|
+
}
|
|
145
|
+
function safeParse(text) {
|
|
146
|
+
if (text.trim() === "") return {};
|
|
147
|
+
try {
|
|
148
|
+
return JSON.parse(text);
|
|
149
|
+
} catch {
|
|
150
|
+
return {};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function parseSuccessBody(res, text) {
|
|
154
|
+
if (res.status === 204) return {};
|
|
155
|
+
if (text !== null && text.trim() !== "") {
|
|
156
|
+
try {
|
|
157
|
+
return JSON.parse(text);
|
|
158
|
+
} catch {
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
throw new BillowApiError(
|
|
162
|
+
res.status,
|
|
163
|
+
"invalid_response",
|
|
164
|
+
`billow returned an empty or unreadable response body (HTTP ${res.status})`,
|
|
165
|
+
void 0,
|
|
166
|
+
requestIdOf(res)
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
function errorFrom(res, data) {
|
|
170
|
+
const err = data.error ?? {};
|
|
171
|
+
return new BillowApiError(
|
|
172
|
+
res.status,
|
|
173
|
+
err.code ?? "error",
|
|
174
|
+
err.message ?? String(res.status),
|
|
175
|
+
err.details,
|
|
176
|
+
requestIdOf(res)
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
async function apiRequestBinary(ctx, method, path, options) {
|
|
180
|
+
const res = await sendWithResilience(
|
|
181
|
+
ctx,
|
|
182
|
+
`${ctx.baseUrl}${path}`,
|
|
183
|
+
{ method, headers: { authorization: `Bearer ${ctx.bearer}` } },
|
|
184
|
+
{
|
|
185
|
+
timeoutMs: options?.timeoutMs ?? ctx.timeoutMs,
|
|
186
|
+
safeToRepeat: safeToRepeat(method, false),
|
|
187
|
+
callerSignal: options?.signal
|
|
188
|
+
}
|
|
189
|
+
);
|
|
190
|
+
if (!res.ok) throw errorFrom(res, safeParse(await res.text().catch(() => "")));
|
|
191
|
+
return res.arrayBuffer();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// src/error-codes.ts
|
|
195
|
+
var BILLOW_ERROR_CODES = [
|
|
196
|
+
"not_found",
|
|
197
|
+
"validation_error",
|
|
198
|
+
"authentication_error",
|
|
199
|
+
"api_key_expired",
|
|
200
|
+
"permission_denied",
|
|
201
|
+
"conflict",
|
|
202
|
+
"invalid_state_transition",
|
|
203
|
+
"provider_error",
|
|
204
|
+
"rate_limited",
|
|
205
|
+
"quota_exceeded",
|
|
206
|
+
"internal_error",
|
|
207
|
+
// Prepaid credits. 402 is shared with `quota_exceeded`: switch on the code, never the status.
|
|
208
|
+
"insufficient_credits",
|
|
209
|
+
"account_frozen",
|
|
210
|
+
"account_closed",
|
|
211
|
+
"idempotency_conflict",
|
|
212
|
+
"commit_exceeds_hold",
|
|
213
|
+
"reversal_exceeds_consumption"
|
|
214
|
+
];
|
|
215
|
+
|
|
216
|
+
// src/money.ts
|
|
217
|
+
var CURRENCY_EXPONENTS = {
|
|
218
|
+
// Zero-decimal
|
|
219
|
+
BIF: 0,
|
|
220
|
+
CLP: 0,
|
|
221
|
+
DJF: 0,
|
|
222
|
+
GNF: 0,
|
|
223
|
+
ISK: 0,
|
|
224
|
+
JPY: 0,
|
|
225
|
+
KMF: 0,
|
|
226
|
+
KRW: 0,
|
|
227
|
+
PYG: 0,
|
|
228
|
+
RWF: 0,
|
|
229
|
+
UGX: 0,
|
|
230
|
+
VND: 0,
|
|
231
|
+
VUV: 0,
|
|
232
|
+
XAF: 0,
|
|
233
|
+
XOF: 0,
|
|
234
|
+
XPF: 0,
|
|
235
|
+
// Three-decimal
|
|
236
|
+
BHD: 3,
|
|
237
|
+
IQD: 3,
|
|
238
|
+
JOD: 3,
|
|
239
|
+
KWD: 3,
|
|
240
|
+
LYD: 3,
|
|
241
|
+
OMR: 3,
|
|
242
|
+
TND: 3,
|
|
243
|
+
// Four-decimal
|
|
244
|
+
CLF: 4,
|
|
245
|
+
UYW: 4
|
|
246
|
+
};
|
|
247
|
+
function currencyExponent(currency) {
|
|
248
|
+
return CURRENCY_EXPONENTS[currency.toUpperCase()] ?? 2;
|
|
249
|
+
}
|
|
250
|
+
function toMinorUnits(major, currency) {
|
|
251
|
+
return Math.round(major * 10 ** currencyExponent(currency));
|
|
252
|
+
}
|
|
253
|
+
function toMajorUnits(minor, currency) {
|
|
254
|
+
return minor / 10 ** currencyExponent(currency);
|
|
255
|
+
}
|
|
256
|
+
function formatMoney(minor, currency) {
|
|
257
|
+
const exponent = currencyExponent(currency);
|
|
258
|
+
const major = minor / 10 ** exponent;
|
|
259
|
+
try {
|
|
260
|
+
return new Intl.NumberFormat("en", {
|
|
261
|
+
style: "currency",
|
|
262
|
+
currency,
|
|
263
|
+
minimumFractionDigits: exponent,
|
|
264
|
+
maximumFractionDigits: exponent
|
|
265
|
+
}).format(major);
|
|
266
|
+
} catch {
|
|
267
|
+
return `${major.toFixed(exponent)} ${currency}`;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// src/types/admin-integrations.ts
|
|
272
|
+
function isSecretField(field) {
|
|
273
|
+
return field.secret === true || field.type === "password";
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// src/pagination.ts
|
|
277
|
+
function makePagedList(fetchFirst, startWalk) {
|
|
278
|
+
let firstPage;
|
|
279
|
+
const first = () => {
|
|
280
|
+
if (!firstPage) {
|
|
281
|
+
const p = fetchFirst();
|
|
282
|
+
firstPage = p;
|
|
283
|
+
p.catch(() => {
|
|
284
|
+
if (firstPage === p) firstPage = void 0;
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
return firstPage;
|
|
288
|
+
};
|
|
289
|
+
async function* iterate() {
|
|
290
|
+
const next = startWalk();
|
|
291
|
+
let page = await first();
|
|
292
|
+
while (page) {
|
|
293
|
+
yield* page.data;
|
|
294
|
+
const following = next(page);
|
|
295
|
+
page = following ? await following : null;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const listAll = async () => {
|
|
299
|
+
const out = [];
|
|
300
|
+
for await (const item of iterate()) out.push(item);
|
|
301
|
+
return out;
|
|
302
|
+
};
|
|
303
|
+
return Object.assign(first(), {
|
|
304
|
+
[Symbol.asyncIterator]: iterate,
|
|
305
|
+
listAll
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
function makeListPromise(fetchPage, params) {
|
|
309
|
+
return makePagedList(
|
|
310
|
+
() => fetchPage(params),
|
|
311
|
+
() => {
|
|
312
|
+
let limit;
|
|
313
|
+
let offset = params.offset ?? 0;
|
|
314
|
+
return (page) => {
|
|
315
|
+
limit ??= page.limit > 0 ? page.limit : params.limit ?? 0;
|
|
316
|
+
offset += page.data.length;
|
|
317
|
+
if (!(limit > 0 && page.data.length >= limit)) return null;
|
|
318
|
+
return fetchPage({ ...params, limit, offset });
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
function makeCursorListPromise(fetchPage, params) {
|
|
324
|
+
return makePagedList(
|
|
325
|
+
() => fetchPage(params),
|
|
326
|
+
() => (page) => page.nextCursor === null ? null : fetchPage({ ...params, limit: page.limit, cursor: page.nextCursor })
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// src/resources/charges.ts
|
|
331
|
+
function createChargesResource(ctx) {
|
|
332
|
+
return {
|
|
333
|
+
/** Create a customer-present charge -> returns a hosted checkout URL. Pass an
|
|
334
|
+
* `idempotencyKey` to make the create safe to retry (with `maxRetries`). */
|
|
335
|
+
create: (input, options) => apiRequest(ctx, "POST", "/v1/charges", input, options),
|
|
336
|
+
/** Charge the customer's current saved card without customer presence. */
|
|
337
|
+
createMerchantInitiated: (input, options) => apiRequest(ctx, "POST", "/v1/charges/mit", input, options),
|
|
338
|
+
/** Fetch a charge. Pass `{ sync: true }` to force an authoritative provider pull. */
|
|
339
|
+
get: (id, opts) => apiRequest(
|
|
340
|
+
ctx,
|
|
341
|
+
"GET",
|
|
342
|
+
`/v1/charges/${encodeURIComponent(id)}${opts?.sync ? "?sync=true" : ""}`,
|
|
343
|
+
void 0,
|
|
344
|
+
opts
|
|
345
|
+
),
|
|
346
|
+
/** List charges (paginated). Filters: status, currency, customer (external id), from, to. */
|
|
347
|
+
list: (params = {}, options) => makeListPromise(
|
|
348
|
+
(p) => apiRequest(
|
|
349
|
+
ctx,
|
|
350
|
+
"GET",
|
|
351
|
+
`/v1/charges${toQuery(p)}`,
|
|
352
|
+
void 0,
|
|
353
|
+
options
|
|
354
|
+
),
|
|
355
|
+
params
|
|
356
|
+
),
|
|
357
|
+
/** Download the one-off charge's receipt as a PDF (raw bytes). */
|
|
358
|
+
receipt: (id, options) => apiRequestBinary(ctx, "GET", `/v1/charges/${encodeURIComponent(id)}/receipt.pdf`, options),
|
|
359
|
+
/** Download the immutable credit note for a succeeded refund. */
|
|
360
|
+
creditNote: (chargeId, refundId, options) => apiRequestBinary(
|
|
361
|
+
ctx,
|
|
362
|
+
"GET",
|
|
363
|
+
`/v1/charges/${encodeURIComponent(chargeId)}/refunds/${encodeURIComponent(refundId)}/credit-note.pdf`,
|
|
364
|
+
options
|
|
365
|
+
),
|
|
366
|
+
/**
|
|
367
|
+
* Refund a succeeded charge: `{ amount }` (minor units) for a partial, `{}` for its whole
|
|
368
|
+
* remaining balance. The `idempotencyKey` is required and must identify this one refund
|
|
369
|
+
* (e.g. your own refund request's id): a retry under it - yours, or the SDK's own
|
|
370
|
+
* `maxRetries` - answers the same refund, in whatever status, instead of refunding again.
|
|
371
|
+
* Reuse the key whenever the outcome is unknown (a timeout or dropped connection); a new
|
|
372
|
+
* key asks for a new refund, e.g. after this one `failed`. `replayed` tells the refund an
|
|
373
|
+
* earlier request under the key made (`true`) from one this call made (`false`).
|
|
374
|
+
*
|
|
375
|
+
* Always check `status`: resolving does not mean refunded. A provider that declines the
|
|
376
|
+
* refund answers `502 provider_error`, which throws with `maxRetries: 0` (the default); with
|
|
377
|
+
* `maxRetries > 0` the SDK retries that 502 under the same key, so the call resolves instead
|
|
378
|
+
* with the declined refund replayed: `status: "failed"`, `replayed: true`.
|
|
379
|
+
*/
|
|
380
|
+
refund: async (chargeId, input, options) => {
|
|
381
|
+
const { status, data } = await apiRequestWithStatus(
|
|
382
|
+
ctx,
|
|
383
|
+
"POST",
|
|
384
|
+
`/v1/charges/${encodeURIComponent(chargeId)}/refund`,
|
|
385
|
+
input,
|
|
386
|
+
options
|
|
387
|
+
);
|
|
388
|
+
return { ...data, replayed: status === 200 };
|
|
389
|
+
},
|
|
390
|
+
/** A charge's refunds, newest first (`needsReview` flags one awaiting an Operator). */
|
|
391
|
+
listRefunds: (chargeId, options) => apiRequest(
|
|
392
|
+
ctx,
|
|
393
|
+
"GET",
|
|
394
|
+
`/v1/charges/${encodeURIComponent(chargeId)}/refunds`,
|
|
395
|
+
void 0,
|
|
396
|
+
options
|
|
397
|
+
),
|
|
398
|
+
/** Reconcile a pending refund with the provider now; resolves to the refund as it stands. */
|
|
399
|
+
recheckRefund: (chargeId, refundId, options) => apiRequest(
|
|
400
|
+
ctx,
|
|
401
|
+
"POST",
|
|
402
|
+
`${refundPath(chargeId, refundId)}/recheck`,
|
|
403
|
+
void 0,
|
|
404
|
+
options
|
|
405
|
+
),
|
|
406
|
+
/**
|
|
407
|
+
* Record the outcome you verified in the provider dashboard for a refund awaiting review
|
|
408
|
+
* (`needsReview`): `succeeded` settles it with a credit note, `failed` releases its amount.
|
|
409
|
+
* A refund with a provider conflict (`providerConflictStatus`) resolves only as `succeeded`.
|
|
410
|
+
*/
|
|
411
|
+
resolveRefund: (chargeId, refundId, input, options) => apiRequest(ctx, "POST", `${refundPath(chargeId, refundId)}/resolve`, input, options)
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
function refundPath(chargeId, refundId) {
|
|
415
|
+
return `/v1/charges/${encodeURIComponent(chargeId)}/refunds/${encodeURIComponent(refundId)}`;
|
|
416
|
+
}
|
|
417
|
+
function createCheckoutMethodsResource(ctx) {
|
|
418
|
+
return {
|
|
419
|
+
/** The customer-present methods + fees offered for a currency (for a method picker). */
|
|
420
|
+
list: (params, options) => apiRequest(
|
|
421
|
+
ctx,
|
|
422
|
+
"GET",
|
|
423
|
+
`/v1/checkout-methods${toQuery(params)}`,
|
|
424
|
+
void 0,
|
|
425
|
+
options
|
|
426
|
+
)
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// src/resources/credits.ts
|
|
431
|
+
function createCreditsResource(ctx) {
|
|
432
|
+
return {
|
|
433
|
+
grants: {
|
|
434
|
+
/**
|
|
435
|
+
* Grant prepaid credits to a Customer. The `idempotencyKey` is required and must identify
|
|
436
|
+
* this one grant (e.g. your own promotion or support-ticket id): a retry under it - yours,
|
|
437
|
+
* or the SDK's own `maxRetries` - answers the same grant instead of granting twice, with
|
|
438
|
+
* `replayed: true`. Reusing the key for a different grant throws `idempotency_conflict`
|
|
439
|
+
* (409). Credit quantities are decimal strings of microcredits in both directions.
|
|
440
|
+
*/
|
|
441
|
+
create: async (input, options) => {
|
|
442
|
+
const { status, data } = await apiRequestWithStatus(
|
|
443
|
+
ctx,
|
|
444
|
+
"POST",
|
|
445
|
+
"/v1/credits/grants",
|
|
446
|
+
input,
|
|
447
|
+
options
|
|
448
|
+
);
|
|
449
|
+
return { ...data, replayed: status === 200 };
|
|
450
|
+
}
|
|
451
|
+
},
|
|
452
|
+
packs: {
|
|
453
|
+
/**
|
|
454
|
+
* The Credit Packs a customer can buy in `currency` (ISO 4217), cheapest first: credits and
|
|
455
|
+
* bonus (decimal strings of microcredits), price (minor units), and the derived
|
|
456
|
+
* `pricePerCredit` and `savingsBps` - so your pricing page never hardcodes a price.
|
|
457
|
+
*/
|
|
458
|
+
list: (currency, options) => apiRequest(
|
|
459
|
+
ctx,
|
|
460
|
+
"GET",
|
|
461
|
+
`/v1/credits/packs${toQuery({ currency })}`,
|
|
462
|
+
void 0,
|
|
463
|
+
options
|
|
464
|
+
),
|
|
465
|
+
/** Configure a Credit Pack on a one-time catalog Price. */
|
|
466
|
+
create: (input, options) => apiRequest(ctx, "POST", "/v1/credits/packs", input, options),
|
|
467
|
+
/** A Credit Pack's configuration, archived or not. */
|
|
468
|
+
get: (packId, options) => apiRequest(
|
|
469
|
+
ctx,
|
|
470
|
+
"GET",
|
|
471
|
+
`/v1/credits/packs/${encodeURIComponent(packId)}`,
|
|
472
|
+
void 0,
|
|
473
|
+
options
|
|
474
|
+
),
|
|
475
|
+
/** Edit a pack's terms or Price, or archive it; top-ups already made keep their terms. */
|
|
476
|
+
update: (packId, input, options) => apiRequest(
|
|
477
|
+
ctx,
|
|
478
|
+
"PATCH",
|
|
479
|
+
`/v1/credits/packs/${encodeURIComponent(packId)}`,
|
|
480
|
+
input,
|
|
481
|
+
options
|
|
482
|
+
)
|
|
483
|
+
},
|
|
484
|
+
topUps: {
|
|
485
|
+
/**
|
|
486
|
+
* Buy a Credit Pack for a customer: answers the top-up with `checkoutUrl`, the hosted
|
|
487
|
+
* checkout to send the buyer to. Credits are granted when the payment succeeds (listen for
|
|
488
|
+
* `credit_top_up.succeeded`). The `idempotencyKey` is required and must identify this one
|
|
489
|
+
* purchase: a retry under it answers the same top-up as it now stands (`replayed: true`) and
|
|
490
|
+
* buys nothing again; reusing it for another purchase throws `idempotency_conflict` (409).
|
|
491
|
+
* A retry can also throw `conflict` (409), with `details.reason`: `checkout_in_progress` -
|
|
492
|
+
* the first request is still opening the checkout, so retry the same key shortly - or
|
|
493
|
+
* `checkout_unavailable` - its checkout could not be opened and never will be, so buy again
|
|
494
|
+
* under a new key.
|
|
495
|
+
*/
|
|
496
|
+
create: async (input, options) => {
|
|
497
|
+
const { status, data } = await apiRequestWithStatus(
|
|
498
|
+
ctx,
|
|
499
|
+
"POST",
|
|
500
|
+
"/v1/credits/top-ups",
|
|
501
|
+
input,
|
|
502
|
+
options
|
|
503
|
+
);
|
|
504
|
+
return { ...data, replayed: status === 200 };
|
|
505
|
+
},
|
|
506
|
+
/** A top-up as it now stands: status, checkout, grants, and what refunds revoked. */
|
|
507
|
+
get: (topUpId, options) => apiRequest(
|
|
508
|
+
ctx,
|
|
509
|
+
"GET",
|
|
510
|
+
`/v1/credits/top-ups/${encodeURIComponent(topUpId)}`,
|
|
511
|
+
void 0,
|
|
512
|
+
options
|
|
513
|
+
),
|
|
514
|
+
/**
|
|
515
|
+
* A customer's top-ups (by external id), newest first, optionally of one status.
|
|
516
|
+
* Auto-paginating by cursor: `await` the first page, `for await (…)` every top-up, or
|
|
517
|
+
* `.listAll()` to collect them.
|
|
518
|
+
*/
|
|
519
|
+
list: (customer, params = {}, options) => {
|
|
520
|
+
const { status, ...page } = params;
|
|
521
|
+
return makeCursorListPromise(
|
|
522
|
+
(p) => apiRequest(
|
|
523
|
+
ctx,
|
|
524
|
+
"GET",
|
|
525
|
+
`/v1/credits/top-ups${toQuery({ customer, status, ...p })}`,
|
|
526
|
+
void 0,
|
|
527
|
+
options
|
|
528
|
+
),
|
|
529
|
+
page
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
},
|
|
533
|
+
balance: {
|
|
534
|
+
/**
|
|
535
|
+
* A Customer's credit balance (by external id): spendable, held, consumed this period,
|
|
536
|
+
* expiring soon, by kind and category, the current period and the threshold level, true at
|
|
537
|
+
* `asOf`. A Customer with no credit account yet reads as zeros.
|
|
538
|
+
*/
|
|
539
|
+
get: (customer, options) => apiRequest(
|
|
540
|
+
ctx,
|
|
541
|
+
"GET",
|
|
542
|
+
`/v1/credits/balance${toQuery({ customer })}`,
|
|
543
|
+
void 0,
|
|
544
|
+
options
|
|
545
|
+
)
|
|
546
|
+
},
|
|
547
|
+
ledger: {
|
|
548
|
+
/**
|
|
549
|
+
* A Customer's credit ledger (by external id), newest first. Auto-paginating by cursor:
|
|
550
|
+
* `await` the first page, `for await (…)` every entry, or `.listAll()` to collect them -
|
|
551
|
+
* entries arriving meanwhile never make the walk skip or repeat one.
|
|
552
|
+
*/
|
|
553
|
+
list: (customer, params = {}, options) => makeCursorListPromise(
|
|
554
|
+
(p) => apiRequest(
|
|
555
|
+
ctx,
|
|
556
|
+
"GET",
|
|
557
|
+
`/v1/credits/ledger${toQuery({ customer, ...p })}`,
|
|
558
|
+
void 0,
|
|
559
|
+
options
|
|
560
|
+
),
|
|
561
|
+
params
|
|
562
|
+
)
|
|
563
|
+
},
|
|
564
|
+
usage: {
|
|
565
|
+
/**
|
|
566
|
+
* A Customer's credit consumption (by external id) per UTC day, by Credit Action or
|
|
567
|
+
* category, over at most 92 days (the 30 ending today by default).
|
|
568
|
+
*/
|
|
569
|
+
get: (customer, params = {}, options) => apiRequest(
|
|
570
|
+
ctx,
|
|
571
|
+
"GET",
|
|
572
|
+
`/v1/credits/usage${toQuery({ customer, ...params })}`,
|
|
573
|
+
void 0,
|
|
574
|
+
options
|
|
575
|
+
)
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// src/resources/customers.ts
|
|
581
|
+
function createCustomersResource(ctx) {
|
|
582
|
+
return {
|
|
583
|
+
create: (input) => apiRequest(ctx, "POST", "/v1/customers", input),
|
|
584
|
+
get: (externalId, options) => apiRequest(
|
|
585
|
+
ctx,
|
|
586
|
+
"GET",
|
|
587
|
+
`/v1/customers/${encodeURIComponent(externalId)}`,
|
|
588
|
+
void 0,
|
|
589
|
+
options
|
|
590
|
+
),
|
|
591
|
+
/** List customers (`q` searches external id / email / name). Auto-paginating: `await` the
|
|
592
|
+
* first page, `for await (…)` every page, or `.listAll()` to collect them. */
|
|
593
|
+
list: (params = {}, options) => makeListPromise(
|
|
594
|
+
(p) => apiRequest(
|
|
595
|
+
ctx,
|
|
596
|
+
"GET",
|
|
597
|
+
`/v1/customers${toQuery(p)}`,
|
|
598
|
+
void 0,
|
|
599
|
+
options
|
|
600
|
+
),
|
|
601
|
+
params
|
|
602
|
+
),
|
|
603
|
+
/** Full per-customer view: profile + subscriptions + charges + cards + balances. */
|
|
604
|
+
overview: (externalId, options) => apiRequest(
|
|
605
|
+
ctx,
|
|
606
|
+
"GET",
|
|
607
|
+
`/v1/customers/${encodeURIComponent(externalId)}/overview`,
|
|
608
|
+
void 0,
|
|
609
|
+
options
|
|
610
|
+
),
|
|
611
|
+
paymentMethods: {
|
|
612
|
+
list: (customer, options) => apiRequest(
|
|
613
|
+
ctx,
|
|
614
|
+
"GET",
|
|
615
|
+
`/v1/customers/${encodeURIComponent(customer)}/payment-methods`,
|
|
616
|
+
void 0,
|
|
617
|
+
options
|
|
618
|
+
).then((r) => r.paymentMethods),
|
|
619
|
+
setDefault: (customer, id) => apiRequest(
|
|
620
|
+
ctx,
|
|
621
|
+
"POST",
|
|
622
|
+
`/v1/customers/${encodeURIComponent(customer)}/payment-methods/${encodeURIComponent(id)}/default`
|
|
623
|
+
),
|
|
624
|
+
remove: (customer, id) => apiRequest(
|
|
625
|
+
ctx,
|
|
626
|
+
"DELETE",
|
|
627
|
+
`/v1/customers/${encodeURIComponent(customer)}/payment-methods/${encodeURIComponent(id)}`
|
|
628
|
+
)
|
|
629
|
+
},
|
|
630
|
+
/** Export all portable customer data held by Billow. Secret-key only. */
|
|
631
|
+
exportData: (externalId, options) => apiRequest(
|
|
632
|
+
ctx,
|
|
633
|
+
"GET",
|
|
634
|
+
`/v1/customers/${encodeURIComponent(externalId)}/export`,
|
|
635
|
+
void 0,
|
|
636
|
+
options
|
|
637
|
+
),
|
|
638
|
+
/** Anonymize PII after access-granting subscriptions have ended. Secret-key only. */
|
|
639
|
+
anonymize: (externalId, options) => apiRequest(
|
|
640
|
+
ctx,
|
|
641
|
+
"DELETE",
|
|
642
|
+
`/v1/customers/${encodeURIComponent(externalId)}`,
|
|
643
|
+
void 0,
|
|
644
|
+
options
|
|
645
|
+
)
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// src/resources/deliverability.ts
|
|
650
|
+
function createDeliverabilityResource(ctx) {
|
|
651
|
+
return {
|
|
652
|
+
/** Email deliveries (newest first), optionally filtered by status. Auto-paginating. */
|
|
653
|
+
emails: (params = {}, options) => makeListPromise(
|
|
654
|
+
(p) => apiRequest(
|
|
655
|
+
ctx,
|
|
656
|
+
"GET",
|
|
657
|
+
`/v1/deliverability/emails${toQuery(p)}`,
|
|
658
|
+
void 0,
|
|
659
|
+
options
|
|
660
|
+
),
|
|
661
|
+
params
|
|
662
|
+
),
|
|
663
|
+
/** Outbound events (newest first), optionally filtered by status. Auto-paginating. */
|
|
664
|
+
events: (params = {}, options) => makeListPromise(
|
|
665
|
+
(p) => apiRequest(
|
|
666
|
+
ctx,
|
|
667
|
+
"GET",
|
|
668
|
+
`/v1/deliverability/events${toQuery(p)}`,
|
|
669
|
+
void 0,
|
|
670
|
+
options
|
|
671
|
+
),
|
|
672
|
+
params
|
|
673
|
+
),
|
|
674
|
+
/** Inbound provider webhooks (newest first), optionally filtered by outcome. Auto-paginating. */
|
|
675
|
+
inbound: (params = {}, options) => makeListPromise(
|
|
676
|
+
(p) => apiRequest(
|
|
677
|
+
ctx,
|
|
678
|
+
"GET",
|
|
679
|
+
`/v1/deliverability/inbound${toQuery(p)}`,
|
|
680
|
+
void 0,
|
|
681
|
+
options
|
|
682
|
+
),
|
|
683
|
+
params
|
|
684
|
+
)
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// src/resources/invoices.ts
|
|
689
|
+
function createInvoicesResource(ctx) {
|
|
690
|
+
return {
|
|
691
|
+
/** List invoices. Filters: status, customer (external id), from, to. Auto-paginating. */
|
|
692
|
+
list: (params = {}, options) => makeListPromise(
|
|
693
|
+
(p) => apiRequest(
|
|
694
|
+
ctx,
|
|
695
|
+
"GET",
|
|
696
|
+
`/v1/invoices${toQuery(p)}`,
|
|
697
|
+
void 0,
|
|
698
|
+
options
|
|
699
|
+
),
|
|
700
|
+
params
|
|
701
|
+
),
|
|
702
|
+
/** Fetch an invoice with its line items. */
|
|
703
|
+
get: (id, options) => apiRequest(
|
|
704
|
+
ctx,
|
|
705
|
+
"GET",
|
|
706
|
+
`/v1/invoices/${encodeURIComponent(id)}`,
|
|
707
|
+
void 0,
|
|
708
|
+
options
|
|
709
|
+
),
|
|
710
|
+
/**
|
|
711
|
+
* Record that an open invoice was paid outside the gateway (bank transfer, cash, cheque).
|
|
712
|
+
* Settles the invoice and advances the subscription exactly like a gateway payment.
|
|
713
|
+
* `amount` must equal the invoice total. Operator (secret key) only.
|
|
714
|
+
*/
|
|
715
|
+
recordPayment: (id, input) => apiRequest(
|
|
716
|
+
ctx,
|
|
717
|
+
"POST",
|
|
718
|
+
`/v1/invoices/${encodeURIComponent(id)}/record-payment`,
|
|
719
|
+
input
|
|
720
|
+
),
|
|
721
|
+
/** Open or resume hosted checkout for an existing outstanding invoice. */
|
|
722
|
+
pay: (id, input = {}, options) => apiRequest(
|
|
723
|
+
ctx,
|
|
724
|
+
"POST",
|
|
725
|
+
`/v1/invoices/${encodeURIComponent(id)}/pay`,
|
|
726
|
+
input,
|
|
727
|
+
options
|
|
728
|
+
),
|
|
729
|
+
/** Download the invoice as a PDF (raw bytes). */
|
|
730
|
+
pdf: (id, options) => apiRequestBinary(ctx, "GET", `/v1/invoices/${encodeURIComponent(id)}/pdf`, options),
|
|
731
|
+
/** Download the invoice's payment receipt as a PDF (raw bytes; 404 if unpaid). */
|
|
732
|
+
receipt: (id, options) => apiRequestBinary(ctx, "GET", `/v1/invoices/${encodeURIComponent(id)}/receipt.pdf`, options)
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// src/resources/marketplace.ts
|
|
737
|
+
function createMarketplaceResource(ctx) {
|
|
738
|
+
return {
|
|
739
|
+
/** Every available Integration with this tenant's install state (no secret values). */
|
|
740
|
+
catalog: (options) => apiRequest(
|
|
741
|
+
ctx,
|
|
742
|
+
"GET",
|
|
743
|
+
"/v1/marketplace",
|
|
744
|
+
void 0,
|
|
745
|
+
options
|
|
746
|
+
).then((r) => r.catalog),
|
|
747
|
+
/** One catalog entry by slug. */
|
|
748
|
+
get: (slug, options) => apiRequest(
|
|
749
|
+
ctx,
|
|
750
|
+
"GET",
|
|
751
|
+
`/v1/marketplace/${encodeURIComponent(slug)}`,
|
|
752
|
+
void 0,
|
|
753
|
+
options
|
|
754
|
+
).then((r) => r.item),
|
|
755
|
+
/** Install or reconfigure an Integration; `values` are the manifest form's field values. */
|
|
756
|
+
upsert: (slug, values) => apiRequest(ctx, "PUT", `/v1/marketplace/${encodeURIComponent(slug)}`, {
|
|
757
|
+
values
|
|
758
|
+
}).then((r) => r.item),
|
|
759
|
+
/** Enable or disable an installed Integration. */
|
|
760
|
+
setEnabled: (slug, enabled) => apiRequest(
|
|
761
|
+
ctx,
|
|
762
|
+
"POST",
|
|
763
|
+
`/v1/marketplace/${encodeURIComponent(slug)}/enabled`,
|
|
764
|
+
{ enabled }
|
|
765
|
+
).then((r) => r.item),
|
|
766
|
+
/** Uninstall an Integration. */
|
|
767
|
+
remove: (slug) => apiRequest(
|
|
768
|
+
ctx,
|
|
769
|
+
"DELETE",
|
|
770
|
+
`/v1/marketplace/${encodeURIComponent(slug)}`
|
|
771
|
+
),
|
|
772
|
+
/**
|
|
773
|
+
* Verify an installed Integration's config. An email Integration sends a test
|
|
774
|
+
* message to `to` (returns `{ sent, to }`); an accounting Integration runs a
|
|
775
|
+
* non-mutating connectivity check with no recipient (returns `{ ok: true }`).
|
|
776
|
+
*/
|
|
777
|
+
test: (slug, to) => apiRequest(
|
|
778
|
+
ctx,
|
|
779
|
+
"POST",
|
|
780
|
+
`/v1/marketplace/${encodeURIComponent(slug)}/test`,
|
|
781
|
+
to ? { to } : {}
|
|
782
|
+
)
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// src/resources/metrics.ts
|
|
787
|
+
function createMetricsResource(ctx) {
|
|
788
|
+
return {
|
|
789
|
+
/** KPI cards for a currency over a window (`from`/`to` default to the last 30 days). */
|
|
790
|
+
overview: (params = {}, options) => apiRequest(
|
|
791
|
+
ctx,
|
|
792
|
+
"GET",
|
|
793
|
+
`/v1/metrics/overview${toQuery(params)}`,
|
|
794
|
+
void 0,
|
|
795
|
+
options
|
|
796
|
+
),
|
|
797
|
+
/** A single bucketed metric (revenue / mrr / subs / success rate) over a window. */
|
|
798
|
+
timeseries: (params = {}, options) => apiRequest(
|
|
799
|
+
ctx,
|
|
800
|
+
"GET",
|
|
801
|
+
`/v1/metrics/timeseries${toQuery(params)}`,
|
|
802
|
+
void 0,
|
|
803
|
+
options
|
|
804
|
+
),
|
|
805
|
+
/**
|
|
806
|
+
* KPIs rolled up into the org's reporting currency across all billing currencies
|
|
807
|
+
* (Phase H). Resolves to `null` when no reporting currency is configured.
|
|
808
|
+
*/
|
|
809
|
+
rollup: (params = {}, options) => apiRequest(
|
|
810
|
+
ctx,
|
|
811
|
+
"GET",
|
|
812
|
+
`/v1/metrics/rollup${toQuery(params)}`,
|
|
813
|
+
void 0,
|
|
814
|
+
options
|
|
815
|
+
).then((r) => r.rollup)
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// src/resources/portal-sessions.ts
|
|
820
|
+
function createPortalSessionsResource(ctx) {
|
|
821
|
+
return {
|
|
822
|
+
create: (input) => apiRequest(ctx, "POST", "/v1/portal-sessions", input)
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// src/resources/products.ts
|
|
827
|
+
function createProductsResource(ctx) {
|
|
828
|
+
return {
|
|
829
|
+
/** Define a product (its first active version + prices + entitlements). */
|
|
830
|
+
create: (input) => apiRequest(ctx, "POST", "/v1/products", input),
|
|
831
|
+
/**
|
|
832
|
+
* Update a product: rename, archive/unarchive, and/or edit its prices +
|
|
833
|
+
* entitlements in place.
|
|
834
|
+
*/
|
|
835
|
+
update: (slug, input) => apiRequest(ctx, "PATCH", `/v1/products/${encodeURIComponent(slug)}`, input),
|
|
836
|
+
/** Fetch a product (active version + prices + entitlements) by slug. */
|
|
837
|
+
get: (slug, options) => apiRequest(
|
|
838
|
+
ctx,
|
|
839
|
+
"GET",
|
|
840
|
+
`/v1/products/${encodeURIComponent(slug)}`,
|
|
841
|
+
void 0,
|
|
842
|
+
options
|
|
843
|
+
),
|
|
844
|
+
/** List products. `includeArchived` also returns archived ones (for admin views). */
|
|
845
|
+
list: (params = {}, options) => apiRequest(
|
|
846
|
+
ctx,
|
|
847
|
+
"GET",
|
|
848
|
+
`/v1/products${params.includeArchived ? "?includeArchived=true" : ""}`,
|
|
849
|
+
void 0,
|
|
850
|
+
options
|
|
851
|
+
).then((r) => r.products),
|
|
852
|
+
/** Delete a plan (soft-delete, ADR-0015). */
|
|
853
|
+
delete: (slug) => apiRequest(ctx, "DELETE", `/v1/products/${encodeURIComponent(slug)}`),
|
|
854
|
+
/** Migrate every live subscriber of `fromSlug` onto `toSlug`. */
|
|
855
|
+
migrateSubscribers: (fromSlug, toSlug) => apiRequest(
|
|
856
|
+
ctx,
|
|
857
|
+
"POST",
|
|
858
|
+
`/v1/products/${encodeURIComponent(fromSlug)}/migrate-subscribers`,
|
|
859
|
+
{ toSlug }
|
|
860
|
+
),
|
|
861
|
+
/** Live-subscriber count per product, keyed by product id. */
|
|
862
|
+
subscriberCounts: (options) => apiRequest(
|
|
863
|
+
ctx,
|
|
864
|
+
"GET",
|
|
865
|
+
"/v1/products/subscriber-counts",
|
|
866
|
+
void 0,
|
|
867
|
+
options
|
|
868
|
+
).then((r) => r.counts)
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// src/resources/settings.ts
|
|
873
|
+
function createSettingsResource(ctx) {
|
|
874
|
+
return {
|
|
875
|
+
/** The dunning retry schedule: configured gaps in days (null = default), + the default. */
|
|
876
|
+
dunning: {
|
|
877
|
+
get: (options) => apiRequest(
|
|
878
|
+
ctx,
|
|
879
|
+
"GET",
|
|
880
|
+
"/v1/settings/dunning",
|
|
881
|
+
void 0,
|
|
882
|
+
options
|
|
883
|
+
),
|
|
884
|
+
/** Set the retry gaps (whole days); pass `null`/`[]` to reset to the default. */
|
|
885
|
+
update: (retryDays) => apiRequest(
|
|
886
|
+
ctx,
|
|
887
|
+
"PUT",
|
|
888
|
+
"/v1/settings/dunning",
|
|
889
|
+
{ retryDays }
|
|
890
|
+
)
|
|
891
|
+
},
|
|
892
|
+
/**
|
|
893
|
+
* Usage settlement grace (hours): defers *collection* of a boundary-adjacent priced
|
|
894
|
+
* calendar-meter renewal past the tz month boundary until late usage settles — the billed
|
|
895
|
+
* windows/amount are unchanged, only the invoice timing shifts. `0` disables it (the default);
|
|
896
|
+
* a no-op for any subscription without a priced calendar meter. Cap 72h.
|
|
897
|
+
*/
|
|
898
|
+
usageSettlementGrace: {
|
|
899
|
+
get: (options) => apiRequest(
|
|
900
|
+
ctx,
|
|
901
|
+
"GET",
|
|
902
|
+
"/v1/settings/usage-settlement-grace",
|
|
903
|
+
void 0,
|
|
904
|
+
options
|
|
905
|
+
),
|
|
906
|
+
/** Set the grace window in whole hours (0–72); `0` disables. */
|
|
907
|
+
update: (graceHours) => apiRequest(ctx, "PUT", "/v1/settings/usage-settlement-grace", {
|
|
908
|
+
graceHours
|
|
909
|
+
})
|
|
910
|
+
},
|
|
911
|
+
/** The currency cross-currency reporting rolls up into (null = none configured). */
|
|
912
|
+
reporting: {
|
|
913
|
+
get: (options) => apiRequest(
|
|
914
|
+
ctx,
|
|
915
|
+
"GET",
|
|
916
|
+
"/v1/settings/reporting",
|
|
917
|
+
void 0,
|
|
918
|
+
options
|
|
919
|
+
),
|
|
920
|
+
/** Set the reporting currency (3-letter ISO), or `null` to clear it. */
|
|
921
|
+
update: (currency) => apiRequest(ctx, "PUT", "/v1/settings/reporting", {
|
|
922
|
+
currency
|
|
923
|
+
})
|
|
924
|
+
},
|
|
925
|
+
/** The manually-maintained FX rates the per-invoice reporting snapshot draws on. */
|
|
926
|
+
fxRates: {
|
|
927
|
+
list: (options) => apiRequest(
|
|
928
|
+
ctx,
|
|
929
|
+
"GET",
|
|
930
|
+
"/v1/settings/fx-rates",
|
|
931
|
+
void 0,
|
|
932
|
+
options
|
|
933
|
+
).then((r) => r.rates),
|
|
934
|
+
/** Upsert one `base → quote` rate (`rate` is the decimal quote-per-base, > 0). */
|
|
935
|
+
set: (baseCurrency, quoteCurrency, rate) => apiRequest(ctx, "PUT", "/v1/settings/fx-rates", {
|
|
936
|
+
baseCurrency,
|
|
937
|
+
quoteCurrency,
|
|
938
|
+
rate
|
|
939
|
+
}),
|
|
940
|
+
/** Remove a `base → quote` rate. */
|
|
941
|
+
remove: (baseCurrency, quoteCurrency) => apiRequest(
|
|
942
|
+
ctx,
|
|
943
|
+
"DELETE",
|
|
944
|
+
`/v1/settings/fx-rates?base=${encodeURIComponent(baseCurrency)}"e=${encodeURIComponent(quoteCurrency)}`
|
|
945
|
+
)
|
|
946
|
+
},
|
|
947
|
+
/** Transactional email templates (PRD-10): per-template subject/message overrides. */
|
|
948
|
+
emailTemplates: {
|
|
949
|
+
/** Every template, merged with this project's overrides, in display order. */
|
|
950
|
+
list: (options) => apiRequest(
|
|
951
|
+
ctx,
|
|
952
|
+
"GET",
|
|
953
|
+
"/v1/settings/email-templates",
|
|
954
|
+
void 0,
|
|
955
|
+
options
|
|
956
|
+
).then((r) => r.templates),
|
|
957
|
+
/** Update one template's override (validated against its variable allowlist). */
|
|
958
|
+
update: (key, input) => apiRequest(
|
|
959
|
+
ctx,
|
|
960
|
+
"PUT",
|
|
961
|
+
`/v1/settings/email-templates/${encodeURIComponent(key)}`,
|
|
962
|
+
input
|
|
963
|
+
).then((r) => r.template),
|
|
964
|
+
/**
|
|
965
|
+
* Send a `[Test]`-prefixed sample of one template to `to`. Dashboard-only:
|
|
966
|
+
* the API rejects developer keys (the dashboard restricts the recipient to
|
|
967
|
+
* the signed-in operator; a developer key has no such guarantee).
|
|
968
|
+
*/
|
|
969
|
+
test: (key, to) => apiRequest(
|
|
970
|
+
ctx,
|
|
971
|
+
"POST",
|
|
972
|
+
`/v1/settings/email-templates/${encodeURIComponent(key)}/test`,
|
|
973
|
+
{ to }
|
|
974
|
+
)
|
|
975
|
+
}
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// src/resources/subscriptions.ts
|
|
980
|
+
function createSubscriptionsResource(ctx) {
|
|
981
|
+
return {
|
|
982
|
+
/** Start a subscription -> returns the subscription plus a hosted checkout URL. `productId`
|
|
983
|
+
* accepts the product's id or slug. Pass an `idempotencyKey` to make it safe to retry. */
|
|
984
|
+
create: (input, options) => apiRequest(ctx, "POST", "/v1/subscriptions", input, options),
|
|
985
|
+
/** Grant a free comp subscription (operator action). `productId` accepts the product's id or slug. */
|
|
986
|
+
comp: (input) => apiRequest(ctx, "POST", "/v1/subscriptions", {
|
|
987
|
+
...input,
|
|
988
|
+
comp: true
|
|
989
|
+
}),
|
|
990
|
+
/** List subscriptions (paginated). Filters: status, customer (external id). */
|
|
991
|
+
list: (params = {}, options) => makeListPromise(
|
|
992
|
+
(p) => apiRequest(
|
|
993
|
+
ctx,
|
|
994
|
+
"GET",
|
|
995
|
+
`/v1/subscriptions${toQuery(p)}`,
|
|
996
|
+
void 0,
|
|
997
|
+
options
|
|
998
|
+
),
|
|
999
|
+
params
|
|
1000
|
+
),
|
|
1001
|
+
get: (id, options) => apiRequest(
|
|
1002
|
+
ctx,
|
|
1003
|
+
"GET",
|
|
1004
|
+
`/v1/subscriptions/${encodeURIComponent(id)}`,
|
|
1005
|
+
void 0,
|
|
1006
|
+
options
|
|
1007
|
+
),
|
|
1008
|
+
/**
|
|
1009
|
+
* The single subscription that currently determines the customer's access, resolved by
|
|
1010
|
+
* billow's own rule (access-granting statuses first, then newest) - or `null` if the
|
|
1011
|
+
* customer has none. Prefer this over picking from `list()` yourself: it avoids the
|
|
1012
|
+
* "newer failed-checkout subscription shadows an older active one" selection bug.
|
|
1013
|
+
*/
|
|
1014
|
+
current: (params, options) => apiRequest(
|
|
1015
|
+
ctx,
|
|
1016
|
+
"GET",
|
|
1017
|
+
`/v1/subscriptions/current${toQuery(params)}`,
|
|
1018
|
+
void 0,
|
|
1019
|
+
options
|
|
1020
|
+
),
|
|
1021
|
+
/** Cancel — immediately, or at period end with `{ atPeriodEnd: true }`. */
|
|
1022
|
+
cancel: (id, opts) => apiRequest(
|
|
1023
|
+
ctx,
|
|
1024
|
+
"POST",
|
|
1025
|
+
`/v1/subscriptions/${encodeURIComponent(id)}/cancel`,
|
|
1026
|
+
opts ?? {}
|
|
1027
|
+
),
|
|
1028
|
+
/** Reverse a scheduled period-end cancellation. Duplicate calls are safe. */
|
|
1029
|
+
resumeCancellation: (id) => apiRequest(
|
|
1030
|
+
ctx,
|
|
1031
|
+
"POST",
|
|
1032
|
+
`/v1/subscriptions/${encodeURIComponent(id)}/resume-cancellation`
|
|
1033
|
+
),
|
|
1034
|
+
pause: (id) => apiRequest(ctx, "POST", `/v1/subscriptions/${encodeURIComponent(id)}/pause`),
|
|
1035
|
+
resume: (id) => apiRequest(ctx, "POST", `/v1/subscriptions/${encodeURIComponent(id)}/resume`),
|
|
1036
|
+
/** Apply a coupon to an existing subscription. */
|
|
1037
|
+
applyCoupon: (id, code) => apiRequest(
|
|
1038
|
+
ctx,
|
|
1039
|
+
"POST",
|
|
1040
|
+
`/v1/subscriptions/${encodeURIComponent(id)}/coupon`,
|
|
1041
|
+
{ code }
|
|
1042
|
+
),
|
|
1043
|
+
/** Change plan — immediate prorated upgrade, or downgrade scheduled for period end. `productId` accepts the product's id or slug. */
|
|
1044
|
+
changePlan: (id, productId) => apiRequest(ctx, "POST", `/v1/subscriptions/${encodeURIComponent(id)}/change-plan`, { productId }),
|
|
1045
|
+
/** Clear a pending downgrade. Duplicate calls are safe. */
|
|
1046
|
+
clearScheduledPlanChange: (id) => apiRequest(
|
|
1047
|
+
ctx,
|
|
1048
|
+
"DELETE",
|
|
1049
|
+
`/v1/subscriptions/${encodeURIComponent(id)}/change-plan`
|
|
1050
|
+
),
|
|
1051
|
+
items: {
|
|
1052
|
+
/** List a subscription's items (base + add-ons). */
|
|
1053
|
+
list: (id, options) => apiRequest(
|
|
1054
|
+
ctx,
|
|
1055
|
+
"GET",
|
|
1056
|
+
`/v1/subscriptions/${encodeURIComponent(id)}/items`,
|
|
1057
|
+
void 0,
|
|
1058
|
+
options
|
|
1059
|
+
).then((r) => r.items),
|
|
1060
|
+
/** Attach an add-on Product. `productId` accepts the product's id or slug. */
|
|
1061
|
+
add: (id, productId, quantity) => apiRequest(
|
|
1062
|
+
ctx,
|
|
1063
|
+
"POST",
|
|
1064
|
+
`/v1/subscriptions/${encodeURIComponent(id)}/items`,
|
|
1065
|
+
{ productId, ...quantity != null ? { quantity } : {} }
|
|
1066
|
+
),
|
|
1067
|
+
/** Change an item's quantity (seats). */
|
|
1068
|
+
updateQuantity: (id, itemId, quantity) => apiRequest(
|
|
1069
|
+
ctx,
|
|
1070
|
+
"PATCH",
|
|
1071
|
+
`/v1/subscriptions/${encodeURIComponent(id)}/items/${encodeURIComponent(itemId)}`,
|
|
1072
|
+
{ quantity }
|
|
1073
|
+
),
|
|
1074
|
+
/** Detach an add-on item. */
|
|
1075
|
+
remove: (id, itemId) => apiRequest(
|
|
1076
|
+
ctx,
|
|
1077
|
+
"DELETE",
|
|
1078
|
+
`/v1/subscriptions/${encodeURIComponent(id)}/items/${encodeURIComponent(itemId)}`
|
|
1079
|
+
)
|
|
1080
|
+
}
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// src/resources/webhook-endpoints.ts
|
|
1085
|
+
function createWebhookEndpointsResource(ctx) {
|
|
1086
|
+
return {
|
|
1087
|
+
/** Register an endpoint. The signing secret is returned once. */
|
|
1088
|
+
create: (input) => apiRequest(ctx, "POST", "/v1/webhook-endpoints", input),
|
|
1089
|
+
/** List endpoints (newest first; secrets are never returned). Auto-paginating. */
|
|
1090
|
+
list: (params = {}, options) => makeListPromise(
|
|
1091
|
+
(p) => apiRequest(
|
|
1092
|
+
ctx,
|
|
1093
|
+
"GET",
|
|
1094
|
+
`/v1/webhook-endpoints${toQuery(p)}`,
|
|
1095
|
+
void 0,
|
|
1096
|
+
options
|
|
1097
|
+
),
|
|
1098
|
+
params
|
|
1099
|
+
),
|
|
1100
|
+
/** Fetch one endpoint by id. */
|
|
1101
|
+
get: (id, options) => apiRequest(
|
|
1102
|
+
ctx,
|
|
1103
|
+
"GET",
|
|
1104
|
+
`/v1/webhook-endpoints/${encodeURIComponent(id)}`,
|
|
1105
|
+
void 0,
|
|
1106
|
+
options
|
|
1107
|
+
),
|
|
1108
|
+
/** Update an endpoint's URL, enabled flag, or event filter. */
|
|
1109
|
+
update: (id, input) => apiRequest(
|
|
1110
|
+
ctx,
|
|
1111
|
+
"PATCH",
|
|
1112
|
+
`/v1/webhook-endpoints/${encodeURIComponent(id)}`,
|
|
1113
|
+
input
|
|
1114
|
+
),
|
|
1115
|
+
/** Delete an endpoint and its delivery history. */
|
|
1116
|
+
delete: (id) => apiRequest(ctx, "DELETE", `/v1/webhook-endpoints/${encodeURIComponent(id)}`),
|
|
1117
|
+
/** Rotate the signing secret — the new plaintext is returned once. */
|
|
1118
|
+
rotateSecret: (id) => apiRequest(
|
|
1119
|
+
ctx,
|
|
1120
|
+
"POST",
|
|
1121
|
+
`/v1/webhook-endpoints/${encodeURIComponent(id)}/rotate-secret`
|
|
1122
|
+
),
|
|
1123
|
+
/** List delivery attempts for an endpoint (newest first). Auto-paginating. */
|
|
1124
|
+
deliveries: (id, params = {}, options) => makeListPromise(
|
|
1125
|
+
(p) => apiRequest(
|
|
1126
|
+
ctx,
|
|
1127
|
+
"GET",
|
|
1128
|
+
`/v1/webhook-endpoints/${encodeURIComponent(id)}/deliveries${toQuery(p)}`,
|
|
1129
|
+
void 0,
|
|
1130
|
+
options
|
|
1131
|
+
),
|
|
1132
|
+
params
|
|
1133
|
+
)
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
// src/index.ts
|
|
1138
|
+
var Billow = class {
|
|
1139
|
+
#ctx;
|
|
1140
|
+
customers;
|
|
1141
|
+
charges;
|
|
1142
|
+
checkoutMethods;
|
|
1143
|
+
/** Prepaid credits: grant them to a Customer (quantities are decimal strings of microcredits). */
|
|
1144
|
+
credits;
|
|
1145
|
+
invoices;
|
|
1146
|
+
metrics;
|
|
1147
|
+
webhookEndpoints;
|
|
1148
|
+
products;
|
|
1149
|
+
subscriptions;
|
|
1150
|
+
portalSessions;
|
|
1151
|
+
deliverability;
|
|
1152
|
+
marketplace;
|
|
1153
|
+
/**
|
|
1154
|
+
* Organization settings (Phase D3, F) — the configurable dunning schedule, usage settlement grace,
|
|
1155
|
+
* and the cross-currency reporting currency + FX-rate registry (ADR-0013).
|
|
1156
|
+
*/
|
|
1157
|
+
settings;
|
|
1158
|
+
constructor(apiKey, opts = {}) {
|
|
1159
|
+
if (!apiKey) throw new Error("billow: an API key is required");
|
|
1160
|
+
this.#ctx = makeContext(apiKey, opts);
|
|
1161
|
+
this.customers = createCustomersResource(this.#ctx);
|
|
1162
|
+
this.charges = createChargesResource(this.#ctx);
|
|
1163
|
+
this.checkoutMethods = createCheckoutMethodsResource(this.#ctx);
|
|
1164
|
+
this.credits = createCreditsResource(this.#ctx);
|
|
1165
|
+
this.invoices = createInvoicesResource(this.#ctx);
|
|
1166
|
+
this.metrics = createMetricsResource(this.#ctx);
|
|
1167
|
+
this.webhookEndpoints = createWebhookEndpointsResource(this.#ctx);
|
|
1168
|
+
this.products = createProductsResource(this.#ctx);
|
|
1169
|
+
this.subscriptions = createSubscriptionsResource(this.#ctx);
|
|
1170
|
+
this.portalSessions = createPortalSessionsResource(this.#ctx);
|
|
1171
|
+
this.deliverability = createDeliverabilityResource(this.#ctx);
|
|
1172
|
+
this.marketplace = createMarketplaceResource(this.#ctx);
|
|
1173
|
+
this.settings = createSettingsResource(this.#ctx);
|
|
1174
|
+
}
|
|
1175
|
+
#request(method, path, body, options) {
|
|
1176
|
+
return apiRequest(this.#ctx, method, path, body, options);
|
|
1177
|
+
}
|
|
1178
|
+
coupons = {
|
|
1179
|
+
/** Define a coupon (a reusable discount template). */
|
|
1180
|
+
create: (input) => this.#request("POST", "/v1/coupons", input),
|
|
1181
|
+
/** List coupons (secret key only — enumerates live promo codes). Auto-paginating. */
|
|
1182
|
+
list: (params = {}, options) => makeListPromise(
|
|
1183
|
+
(p) => this.#request("GET", `/v1/coupons${toQuery(p)}`, void 0, options),
|
|
1184
|
+
params
|
|
1185
|
+
),
|
|
1186
|
+
/** Deactivate a coupon (existing discounts keep running). */
|
|
1187
|
+
deactivate: (id) => this.#request("POST", `/v1/coupons/${encodeURIComponent(id)}/deactivate`)
|
|
1188
|
+
};
|
|
1189
|
+
/** Manage developer webhook endpoints — register, rotate secrets, inspect deliveries.
|
|
1190
|
+
* (To VERIFY incoming deliveries, import `constructEvent` from `@usebillow/sdk/webhooks`.) */
|
|
1191
|
+
features = {
|
|
1192
|
+
/** Define a feature (a boolean access gate, or a metered feature with a meter). */
|
|
1193
|
+
create: (input) => this.#request("POST", "/v1/features", input),
|
|
1194
|
+
/**
|
|
1195
|
+
* Edit a feature in place: its `name`, and — for a metered feature — its `meter`
|
|
1196
|
+
* aggregation. The server refuses a meter change once usage has been recorded (it
|
|
1197
|
+
* would rewrite billed history), so set the meter right at create time or before you
|
|
1198
|
+
* start tracking. Returns the updated feature.
|
|
1199
|
+
*/
|
|
1200
|
+
update: (slug, input) => this.#request("PATCH", `/v1/features/${encodeURIComponent(slug)}`, input),
|
|
1201
|
+
/** List features in the tenant. Auto-paginating. */
|
|
1202
|
+
list: (params = {}, options) => makeListPromise(
|
|
1203
|
+
(p) => this.#request("GET", `/v1/features${toQuery(p)}`, void 0, options),
|
|
1204
|
+
params
|
|
1205
|
+
),
|
|
1206
|
+
/**
|
|
1207
|
+
* Usage Alerts + Spend caps on a metered feature (Phase C). A `threshold`
|
|
1208
|
+
* Alert notifies (webhook + optional customer email) as usage crosses its
|
|
1209
|
+
* thresholds; a `cap` blocks `check` once reached.
|
|
1210
|
+
*/
|
|
1211
|
+
alerts: {
|
|
1212
|
+
/** List the alerts + spend cap on a metered feature (by slug). */
|
|
1213
|
+
list: (feature, options) => this.#request(
|
|
1214
|
+
"GET",
|
|
1215
|
+
`/v1/features/${encodeURIComponent(feature)}/alerts`,
|
|
1216
|
+
void 0,
|
|
1217
|
+
options
|
|
1218
|
+
).then((r) => r.alerts),
|
|
1219
|
+
/** Define an Alert or Spend cap on a metered feature. */
|
|
1220
|
+
create: (feature, input) => this.#request(
|
|
1221
|
+
"POST",
|
|
1222
|
+
`/v1/features/${encodeURIComponent(feature)}/alerts`,
|
|
1223
|
+
input
|
|
1224
|
+
),
|
|
1225
|
+
/** Delete one alert/cap rule by id. */
|
|
1226
|
+
remove: (feature, id) => this.#request(
|
|
1227
|
+
"DELETE",
|
|
1228
|
+
`/v1/features/${encodeURIComponent(feature)}/alerts/${encodeURIComponent(id)}`
|
|
1229
|
+
)
|
|
1230
|
+
}
|
|
1231
|
+
};
|
|
1232
|
+
integrations = {
|
|
1233
|
+
/** The registered payment providers + their config manifests (drives the picker + form). */
|
|
1234
|
+
providers: (options) => this.#request(
|
|
1235
|
+
"GET",
|
|
1236
|
+
"/v1/integrations/providers",
|
|
1237
|
+
void 0,
|
|
1238
|
+
options
|
|
1239
|
+
).then((r) => r.providers),
|
|
1240
|
+
/** List configured credential sets per currency (provider + non-secret config). Secret-only. */
|
|
1241
|
+
list: (options) => this.#request(
|
|
1242
|
+
"GET",
|
|
1243
|
+
"/v1/integrations",
|
|
1244
|
+
void 0,
|
|
1245
|
+
options
|
|
1246
|
+
).then((r) => r.integrations),
|
|
1247
|
+
/**
|
|
1248
|
+
* Verify a provider's configuration against its live API. Pass `currency` to
|
|
1249
|
+
* choose a credential set and `target` to probe one verify target (Paymob: a
|
|
1250
|
+
* method); pass `sampleToken` (SANDBOX ONLY) to run a functional sub-test.
|
|
1251
|
+
* This probe hits the provider's live API (mutates nothing here), so it takes a
|
|
1252
|
+
* per-call {@link CallOptions} for a timeout / cancellation — `opts` is spread into
|
|
1253
|
+
* the body, so `options` stays a separate trailing arg (never merged in).
|
|
1254
|
+
*/
|
|
1255
|
+
verify: (provider, opts, options) => this.#request(
|
|
1256
|
+
"POST",
|
|
1257
|
+
"/v1/integrations/verify",
|
|
1258
|
+
{ provider, ...opts },
|
|
1259
|
+
options
|
|
1260
|
+
),
|
|
1261
|
+
/** Manage stored payment-provider credentials (the dashboard's "manage credentials" forms). */
|
|
1262
|
+
credentials: {
|
|
1263
|
+
/** The editable view of stored credential sets (secrets are never returned). */
|
|
1264
|
+
list: (options) => this.#request(
|
|
1265
|
+
"GET",
|
|
1266
|
+
"/v1/integrations/credentials",
|
|
1267
|
+
void 0,
|
|
1268
|
+
options
|
|
1269
|
+
).then((r) => r.credentials),
|
|
1270
|
+
/** Create or rotate a (provider, currency) credential set. */
|
|
1271
|
+
upsert: (input) => this.#request("PUT", "/v1/integrations/credentials", input),
|
|
1272
|
+
/** Delete a (provider, currency) credential set. */
|
|
1273
|
+
remove: (provider, currency) => this.#request(
|
|
1274
|
+
"DELETE",
|
|
1275
|
+
`/v1/integrations/credentials/${encodeURIComponent(provider)}/${encodeURIComponent(currency)}`
|
|
1276
|
+
)
|
|
1277
|
+
}
|
|
1278
|
+
};
|
|
1279
|
+
/** The merchant's business identity — the seller block on documents and email from-name. */
|
|
1280
|
+
businessProfile = {
|
|
1281
|
+
/** The stored profile, or null if one was never saved. */
|
|
1282
|
+
get: (options) => this.#request(
|
|
1283
|
+
"GET",
|
|
1284
|
+
"/v1/business-profile",
|
|
1285
|
+
void 0,
|
|
1286
|
+
options
|
|
1287
|
+
).then((r) => r.profile),
|
|
1288
|
+
/** Create or update the profile; omit a field to keep it, send "" to clear it. */
|
|
1289
|
+
update: (input) => this.#request("PUT", "/v1/business-profile", input).then(
|
|
1290
|
+
(r) => r.profile
|
|
1291
|
+
)
|
|
1292
|
+
};
|
|
1293
|
+
/**
|
|
1294
|
+
* Hosted customer surfaces (Phase G, ADR-0014). Mint a portal session for one of
|
|
1295
|
+
* your signed-in users and redirect them to the returned `url` — the self-serve
|
|
1296
|
+
* portal, or a `checkout` hand-off for `productId`. The Customer's own calls go
|
|
1297
|
+
* through {@link BillowPortal}, constructed with the session token.
|
|
1298
|
+
*/
|
|
1299
|
+
/** Identity of this key's tenant — org, environment, configured currencies. */
|
|
1300
|
+
me(options) {
|
|
1301
|
+
return this.#request("GET", "/v1/me", void 0, options);
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Global search (PRD-12) across customers, invoices, subscriptions, and charges
|
|
1305
|
+
* for the key's `(project, environment)`. Bounded per entity (default 5, max 10);
|
|
1306
|
+
* a blank query returns empty results. Backs the dashboard's ⌘K palette.
|
|
1307
|
+
*/
|
|
1308
|
+
search(q, limit, options) {
|
|
1309
|
+
return this.#request(
|
|
1310
|
+
"GET",
|
|
1311
|
+
`/v1/search${toQuery({ q, limit })}`,
|
|
1312
|
+
void 0,
|
|
1313
|
+
options
|
|
1314
|
+
);
|
|
1315
|
+
}
|
|
1316
|
+
// ── The headline verbs ──────────────────────────────────────────────
|
|
1317
|
+
/**
|
|
1318
|
+
* Gate access to a feature. `featureId` is the feature slug. Returns
|
|
1319
|
+
* `{ allowed, balance }` — `balance` is `null` for unlimited/boolean features.
|
|
1320
|
+
*/
|
|
1321
|
+
check(input, options) {
|
|
1322
|
+
return this.#request("POST", "/v1/check", input, options);
|
|
1323
|
+
}
|
|
1324
|
+
/**
|
|
1325
|
+
* Record usage of a metered feature (`value` defaults to 1; negative credits
|
|
1326
|
+
* back). Pass `idempotencyKey` to make a retried call a no-op — which also makes the call
|
|
1327
|
+
* safe to retry automatically when `maxRetries` is set.
|
|
1328
|
+
*/
|
|
1329
|
+
track(input, options) {
|
|
1330
|
+
return this.#request(
|
|
1331
|
+
"POST",
|
|
1332
|
+
"/v1/track",
|
|
1333
|
+
input,
|
|
1334
|
+
input.idempotencyKey ? { ...options, idempotencyKey: input.idempotencyKey } : options
|
|
1335
|
+
);
|
|
1336
|
+
}
|
|
1337
|
+
/** List a customer's live entitlements (one per feature). */
|
|
1338
|
+
entitlements(customerId, options) {
|
|
1339
|
+
return this.#request(
|
|
1340
|
+
"GET",
|
|
1341
|
+
`/v1/customers/${encodeURIComponent(customerId)}/entitlements`,
|
|
1342
|
+
void 0,
|
|
1343
|
+
options
|
|
1344
|
+
);
|
|
1345
|
+
}
|
|
1346
|
+
/** Start a subscription → returns a hosted checkout URL to redirect the customer to. */
|
|
1347
|
+
attach(input, options) {
|
|
1348
|
+
return this.#request("POST", "/v1/attach", input, options);
|
|
1349
|
+
}
|
|
1350
|
+
};
|
|
1351
|
+
var BillowPublishable = class {
|
|
1352
|
+
#ctx;
|
|
1353
|
+
constructor(publishableKey, opts = {}) {
|
|
1354
|
+
if (!publishableKey) throw new Error("billow: a publishable key is required");
|
|
1355
|
+
if (!publishableKey.includes("_pk_")) {
|
|
1356
|
+
throw new Error(
|
|
1357
|
+
"billow: BillowPublishable needs a publishable key (bl_<env>_pk_\u2026), not a secret key"
|
|
1358
|
+
);
|
|
1359
|
+
}
|
|
1360
|
+
this.#ctx = makeContext(publishableKey, opts);
|
|
1361
|
+
}
|
|
1362
|
+
products = {
|
|
1363
|
+
/** The active catalog — products and their prices, for a pricing table. */
|
|
1364
|
+
list: (options) => apiRequest(
|
|
1365
|
+
this.#ctx,
|
|
1366
|
+
"GET",
|
|
1367
|
+
"/v1/products",
|
|
1368
|
+
void 0,
|
|
1369
|
+
options
|
|
1370
|
+
).then((r) => r.products),
|
|
1371
|
+
/** Fetch one active product (with its prices) by slug. */
|
|
1372
|
+
get: (slug, options) => apiRequest(
|
|
1373
|
+
this.#ctx,
|
|
1374
|
+
"GET",
|
|
1375
|
+
`/v1/products/${encodeURIComponent(slug)}`,
|
|
1376
|
+
void 0,
|
|
1377
|
+
options
|
|
1378
|
+
)
|
|
1379
|
+
};
|
|
1380
|
+
};
|
|
1381
|
+
var BillowPortal = class {
|
|
1382
|
+
#ctx;
|
|
1383
|
+
constructor(sessionToken, opts = {}) {
|
|
1384
|
+
if (!sessionToken) throw new Error("billow: a portal session token is required");
|
|
1385
|
+
this.#ctx = makeContext(sessionToken, opts);
|
|
1386
|
+
}
|
|
1387
|
+
/** The portal shell: flow, return URL, merchant brand, and customer identity —
|
|
1388
|
+
* the lightweight payload the hosting app frames every page with, and the
|
|
1389
|
+
* validate-and-route check at login. */
|
|
1390
|
+
session(options) {
|
|
1391
|
+
return apiRequest(this.#ctx, "GET", "/portal/session", void 0, options);
|
|
1392
|
+
}
|
|
1393
|
+
/** The self-serve home payload: identity + subscriptions + usage + saved cards. */
|
|
1394
|
+
me(options) {
|
|
1395
|
+
return apiRequest(this.#ctx, "GET", "/portal/me", void 0, options);
|
|
1396
|
+
}
|
|
1397
|
+
invoices = {
|
|
1398
|
+
/** The Customer's invoices (newest first). Auto-paginating. */
|
|
1399
|
+
list: (params = {}, options) => makeListPromise(
|
|
1400
|
+
(p) => apiRequest(
|
|
1401
|
+
this.#ctx,
|
|
1402
|
+
"GET",
|
|
1403
|
+
`/portal/invoices${toQuery(p)}`,
|
|
1404
|
+
void 0,
|
|
1405
|
+
options
|
|
1406
|
+
),
|
|
1407
|
+
params
|
|
1408
|
+
),
|
|
1409
|
+
/** Open or resume checkout for this customer's outstanding invoice. */
|
|
1410
|
+
pay: (id, input = {}, options) => apiRequest(
|
|
1411
|
+
this.#ctx,
|
|
1412
|
+
"POST",
|
|
1413
|
+
`/portal/invoices/${encodeURIComponent(id)}/pay`,
|
|
1414
|
+
input,
|
|
1415
|
+
options
|
|
1416
|
+
),
|
|
1417
|
+
/** Download one of the Customer's invoices as a PDF (raw bytes). */
|
|
1418
|
+
pdf: (id, options) => apiRequestBinary(this.#ctx, "GET", `/portal/invoices/${encodeURIComponent(id)}/pdf`, options),
|
|
1419
|
+
/** Download the payment receipt for one of the Customer's invoices (raw bytes). */
|
|
1420
|
+
receiptPdf: (id, options) => apiRequestBinary(
|
|
1421
|
+
this.#ctx,
|
|
1422
|
+
"GET",
|
|
1423
|
+
`/portal/invoices/${encodeURIComponent(id)}/receipt.pdf`,
|
|
1424
|
+
options
|
|
1425
|
+
)
|
|
1426
|
+
};
|
|
1427
|
+
subscriptions = {
|
|
1428
|
+
/** Schedule one of the Customer's subscriptions to cancel at period end. */
|
|
1429
|
+
cancel: (id) => apiRequest(
|
|
1430
|
+
this.#ctx,
|
|
1431
|
+
"POST",
|
|
1432
|
+
`/portal/subscriptions/${encodeURIComponent(id)}/cancel`
|
|
1433
|
+
),
|
|
1434
|
+
/** Reverse a scheduled period-end cancellation. */
|
|
1435
|
+
resumeCancellation: (id) => apiRequest(
|
|
1436
|
+
this.#ctx,
|
|
1437
|
+
"POST",
|
|
1438
|
+
`/portal/subscriptions/${encodeURIComponent(id)}/resume-cancellation`
|
|
1439
|
+
),
|
|
1440
|
+
/** Move one of the Customer's subscriptions to another plan (upgrade now / downgrade scheduled). */
|
|
1441
|
+
changePlan: (id, productId) => apiRequest(
|
|
1442
|
+
this.#ctx,
|
|
1443
|
+
"POST",
|
|
1444
|
+
`/portal/subscriptions/${encodeURIComponent(id)}/plan`,
|
|
1445
|
+
{ productId }
|
|
1446
|
+
),
|
|
1447
|
+
/** Clear a pending downgrade. */
|
|
1448
|
+
clearScheduledPlanChange: (id) => apiRequest(
|
|
1449
|
+
this.#ctx,
|
|
1450
|
+
"DELETE",
|
|
1451
|
+
`/portal/subscriptions/${encodeURIComponent(id)}/plan`
|
|
1452
|
+
)
|
|
1453
|
+
};
|
|
1454
|
+
paymentMethod = {
|
|
1455
|
+
/** Open a checkout to save or replace the Customer's card (zero-amount tokenization). */
|
|
1456
|
+
update: (returnUrl) => apiRequest(
|
|
1457
|
+
this.#ctx,
|
|
1458
|
+
"POST",
|
|
1459
|
+
"/portal/payment-method/update",
|
|
1460
|
+
returnUrl ? { returnUrl } : {}
|
|
1461
|
+
)
|
|
1462
|
+
};
|
|
1463
|
+
paymentMethods = {
|
|
1464
|
+
list: (options) => apiRequest(
|
|
1465
|
+
this.#ctx,
|
|
1466
|
+
"GET",
|
|
1467
|
+
"/portal/payment-methods",
|
|
1468
|
+
void 0,
|
|
1469
|
+
options
|
|
1470
|
+
).then((r) => r.paymentMethods),
|
|
1471
|
+
setDefault: (id) => apiRequest(
|
|
1472
|
+
this.#ctx,
|
|
1473
|
+
"POST",
|
|
1474
|
+
`/portal/payment-methods/${encodeURIComponent(id)}/default`
|
|
1475
|
+
),
|
|
1476
|
+
remove: (id) => apiRequest(
|
|
1477
|
+
this.#ctx,
|
|
1478
|
+
"DELETE",
|
|
1479
|
+
`/portal/payment-methods/${encodeURIComponent(id)}`
|
|
1480
|
+
)
|
|
1481
|
+
};
|
|
1482
|
+
/** The checkout hand-off, for a session minted with `flow: "checkout"`. */
|
|
1483
|
+
checkout = {
|
|
1484
|
+
/** The plan being confirmed (product + base price). */
|
|
1485
|
+
get: (options) => apiRequest(this.#ctx, "GET", "/portal/checkout", void 0, options),
|
|
1486
|
+
/** Subscribe to the plan → returns the hosted Paymob checkout URL to redirect to. */
|
|
1487
|
+
start: () => apiRequest(this.#ctx, "POST", "/portal/checkout")
|
|
1488
|
+
};
|
|
1489
|
+
};
|
|
1490
|
+
|
|
1491
|
+
export { BILLOW_ERROR_CODES, Billow, BillowApiError, BillowPortal, BillowPublishable, currencyExponent, formatMoney, isSecretField, toMajorUnits, toMinorUnits };
|
|
1492
|
+
//# sourceMappingURL=chunk-Z6VXPONT.js.map
|
|
1493
|
+
//# sourceMappingURL=chunk-Z6VXPONT.js.map
|