@avvio/payments 0.1.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 +66 -0
- package/ERRORS.md +252 -0
- package/LICENSE +21 -0
- package/QUICKSTART.md +317 -0
- package/README.md +411 -0
- package/index.d.ts +680 -0
- package/package.json +55 -0
- package/src/cli.js +635 -0
- package/src/client.js +799 -0
- package/src/mcp.js +434 -0
- package/src/webhooks.js +191 -0
package/index.d.ts
ADDED
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for @avvio/payments.
|
|
3
|
+
*
|
|
4
|
+
* Hand-written rather than generated, and shipped as a plain `.d.ts` alongside
|
|
5
|
+
* the JavaScript: TypeScript as a build step would add a compile to a package
|
|
6
|
+
* whose selling point is that there is nothing to compile, and a devDependency
|
|
7
|
+
* is still a dependency to explain. This file adds no runtime surface at all.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** An amount and its currency. Decimal strings — never floats. */
|
|
11
|
+
export interface Money {
|
|
12
|
+
currency: string;
|
|
13
|
+
amount: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** The whole partner-facing status vocabulary. */
|
|
17
|
+
export type PayoutStatus =
|
|
18
|
+
| 'pending'
|
|
19
|
+
| 'processing'
|
|
20
|
+
| 'completed'
|
|
21
|
+
| 'failed'
|
|
22
|
+
| 'canceled';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Why a payout did not land. Treat an unrecognised value as `execution_failed`
|
|
26
|
+
* — new codes are added without a major version.
|
|
27
|
+
*/
|
|
28
|
+
export type PayoutFailureCode =
|
|
29
|
+
| 'quote_expired'
|
|
30
|
+
| 'insufficient_funds'
|
|
31
|
+
| 'limit_exceeded'
|
|
32
|
+
| 'account_invalid'
|
|
33
|
+
| 'account_cannot_receive'
|
|
34
|
+
| 'compliance_rejected'
|
|
35
|
+
| 'authorization_not_completed'
|
|
36
|
+
| 'returned_by_bank'
|
|
37
|
+
| 'execution_failed'
|
|
38
|
+
| 'canceled_by_platform'
|
|
39
|
+
| 'unknown';
|
|
40
|
+
|
|
41
|
+
/** Informational only. Never branch integration behaviour on it. */
|
|
42
|
+
export type PayoutStage = 'awaiting_details' | 'under_review' | 'settling';
|
|
43
|
+
|
|
44
|
+
export interface EndUser {
|
|
45
|
+
id?: string;
|
|
46
|
+
name?: string;
|
|
47
|
+
email?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface Payout {
|
|
51
|
+
payoutId: string;
|
|
52
|
+
status: PayoutStatus;
|
|
53
|
+
/**
|
|
54
|
+
* Present and true when this payout waits on YOU to fund it from your own
|
|
55
|
+
* wallet. Absent on routings that settle on acceptance, so its presence is
|
|
56
|
+
* the signal — call `getFunding()`.
|
|
57
|
+
*/
|
|
58
|
+
requiresFunding?: boolean;
|
|
59
|
+
/** When this payout last changed. Your `updatedSince` watermark. */
|
|
60
|
+
updatedAt?: string;
|
|
61
|
+
stage?: PayoutStage;
|
|
62
|
+
failureCode?: PayoutFailureCode;
|
|
63
|
+
/**
|
|
64
|
+
* Whether the money is back in your balance. Absent means not established
|
|
65
|
+
* yet — deliberately not `false`, which would be a claim we cannot support.
|
|
66
|
+
*/
|
|
67
|
+
fundsReturned?: boolean;
|
|
68
|
+
sourceAmount: Money | null;
|
|
69
|
+
destinationAmount: Money | null;
|
|
70
|
+
destinationAccountId: string | null;
|
|
71
|
+
rate: string | null;
|
|
72
|
+
reference: string | null;
|
|
73
|
+
endUser: EndUser | null;
|
|
74
|
+
createdAt: string | null;
|
|
75
|
+
completedAt: string | null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface PreviewQuote {
|
|
79
|
+
/** Always true. This is an estimate; the binding price comes at send. */
|
|
80
|
+
indicative: true;
|
|
81
|
+
sourceAmount: Money;
|
|
82
|
+
destinationAmount: Money;
|
|
83
|
+
/** Deducted from the send: destinationAmount = (sourceAmount - fee) x rate. */
|
|
84
|
+
fee: Money;
|
|
85
|
+
totalDebit: Money;
|
|
86
|
+
rate: string;
|
|
87
|
+
limits: { min: string; max: string };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface CorridorField {
|
|
91
|
+
id: string;
|
|
92
|
+
title: string;
|
|
93
|
+
type: string;
|
|
94
|
+
pattern?: string;
|
|
95
|
+
required: boolean;
|
|
96
|
+
description?: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface Corridor {
|
|
100
|
+
currency: string;
|
|
101
|
+
fields: CorridorField[];
|
|
102
|
+
limits?: { min: string; max: string };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface BeneficiaryMethod {
|
|
106
|
+
id: string;
|
|
107
|
+
kind: string;
|
|
108
|
+
currency: string | null;
|
|
109
|
+
last4: string | null;
|
|
110
|
+
status: string;
|
|
111
|
+
/** Pass this as `destinationAccountId` when paying. */
|
|
112
|
+
destinationAccountId: string | null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface Beneficiary {
|
|
116
|
+
id: string;
|
|
117
|
+
name: string;
|
|
118
|
+
email?: string;
|
|
119
|
+
country?: string;
|
|
120
|
+
externalId?: string;
|
|
121
|
+
endUserId?: string | null;
|
|
122
|
+
paymentMethods: BeneficiaryMethod[];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Every `type` the API and this client can produce.
|
|
127
|
+
*
|
|
128
|
+
* Declared as a bare `string` until now, so the `switch (err.type)` the docs
|
|
129
|
+
* insist on could not be checked at all: a typo'd case compiled, an unhandled
|
|
130
|
+
* one compiled, and the branch that decides whether a payment is retried was
|
|
131
|
+
* the least-typed code a partner wrote.
|
|
132
|
+
*
|
|
133
|
+
* `(string & {})` is deliberately kept in the union. New types are added
|
|
134
|
+
* without a major version, so an unrecognised one must still compile — the
|
|
135
|
+
* alternative is a partner pinned to an old package failing to build against a
|
|
136
|
+
* live API. It costs the exhaustiveness check on the `default` branch, which is
|
|
137
|
+
* the branch you should be writing anyway.
|
|
138
|
+
*
|
|
139
|
+
* Kept in step with `docs/partner/ERRORS.md` by a test, not by hand.
|
|
140
|
+
*/
|
|
141
|
+
export type PayoutsErrorType =
|
|
142
|
+
// General
|
|
143
|
+
| 'VALIDATION_ERROR'
|
|
144
|
+
| 'UNAUTHORIZED'
|
|
145
|
+
| 'FORBIDDEN'
|
|
146
|
+
| 'NOT_FOUND'
|
|
147
|
+
| 'RATE_LIMITED'
|
|
148
|
+
| 'BAD_REQUEST'
|
|
149
|
+
| 'PROVIDER_REJECTED'
|
|
150
|
+
| 'FUNDING_TRANSACTION_INVALID'
|
|
151
|
+
| 'FUNDING_NOT_YET_VERIFIABLE'
|
|
152
|
+
| 'FUNDING_TRANSACTION_ALREADY_USED'
|
|
153
|
+
| 'PAYOUT_NOT_FUNDABLE'
|
|
154
|
+
| 'BENEFICIARY_EXTERNAL_ID_CONFLICT'
|
|
155
|
+
| 'CONFLICT'
|
|
156
|
+
| 'ACCOUNT_BLOCKED'
|
|
157
|
+
| 'INSUFFICIENT_BALANCE'
|
|
158
|
+
| 'ORDERS_TEMPORARILY_UNAVAILABLE'
|
|
159
|
+
| 'INTERNAL'
|
|
160
|
+
// Idempotency
|
|
161
|
+
| 'IDEMPOTENCY_KEY_REQUIRED'
|
|
162
|
+
| 'IDEMPOTENCY_KEY_INVALID'
|
|
163
|
+
| 'IDEMPOTENCY_KEY_CONFLICT'
|
|
164
|
+
| 'IDEMPOTENCY_KEY_REQUEST_IN_PROGRESS'
|
|
165
|
+
| 'IDEMPOTENCY_UNAVAILABLE'
|
|
166
|
+
| 'DUPLICATE_REQUEST_DETECTED'
|
|
167
|
+
// Paying
|
|
168
|
+
| 'DESTINATION_ACCOUNT_NOT_FOUND'
|
|
169
|
+
| 'RATE_DRIFT_EXCEEDED'
|
|
170
|
+
| 'QUOTE_UNVERIFIABLE'
|
|
171
|
+
| 'EXACT_OUTPUT_UNSUPPORTED'
|
|
172
|
+
| 'PAYOUT_NOT_CANCELABLE'
|
|
173
|
+
| 'INSUFFICIENT_SCOPE'
|
|
174
|
+
| 'INDICATIVE_PRICING_UNAVAILABLE'
|
|
175
|
+
| 'PAYOUT_LINK_UNUSABLE'
|
|
176
|
+
// Raised by this client, with no HTTP response behind them. `TIMEOUT` is the
|
|
177
|
+
// important one: it is an UNKNOWN outcome, not a failure, and the payout may
|
|
178
|
+
// exist. `CORRIDOR_UNAVAILABLE` is raised here too, and is in the table.
|
|
179
|
+
| 'TIMEOUT'
|
|
180
|
+
| 'NETWORK_ERROR'
|
|
181
|
+
| 'CORRIDOR_UNAVAILABLE'
|
|
182
|
+
| (string & {});
|
|
183
|
+
|
|
184
|
+
export declare class PayoutsError extends Error {
|
|
185
|
+
readonly name: 'PayoutsError';
|
|
186
|
+
/** Stable machine-readable code. Branch on this, not on the message. */
|
|
187
|
+
readonly type: PayoutsErrorType;
|
|
188
|
+
readonly status: number;
|
|
189
|
+
readonly requestId?: string;
|
|
190
|
+
/**
|
|
191
|
+
* The key the failed request used. On a timeout, retry with THIS value — a
|
|
192
|
+
* new one is a second payment.
|
|
193
|
+
*/
|
|
194
|
+
readonly idempotencyKey?: string;
|
|
195
|
+
readonly body?: unknown;
|
|
196
|
+
/** Whether retrying the identical request unchanged is worth doing. */
|
|
197
|
+
readonly retryable: boolean;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export interface PayoutsClientOptions {
|
|
201
|
+
/** Server-side only. Never ship to a browser or a mobile app. */
|
|
202
|
+
apiKey?: string;
|
|
203
|
+
orgId?: string;
|
|
204
|
+
baseUrl?: string;
|
|
205
|
+
timeoutMs?: number;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export interface PayoutArgs {
|
|
209
|
+
amount: string;
|
|
210
|
+
destinationAccountId: string;
|
|
211
|
+
/**
|
|
212
|
+
* What you told the payer they would receive. The send is REFUSED if the
|
|
213
|
+
* binding quote drifts past `maxRateDrift` from it. Without this you send at
|
|
214
|
+
* whatever the rate did between quoting and sending.
|
|
215
|
+
*/
|
|
216
|
+
expectDestinationAmount?: string;
|
|
217
|
+
/**
|
|
218
|
+
* Alias of `expectDestinationAmount`, accepted at runtime and used by every
|
|
219
|
+
* example in the README. It was undeclared, so a TypeScript user copying the
|
|
220
|
+
* documented example got a compile error on the drift guard — the one field
|
|
221
|
+
* the docs insist you should never skip.
|
|
222
|
+
*/
|
|
223
|
+
expectDestination?: string;
|
|
224
|
+
/** Fractional, so 0.02 is 2%. Defaults to 0.02. */
|
|
225
|
+
maxRateDrift?: number;
|
|
226
|
+
/** Reuse to retry safely. Generated if omitted. */
|
|
227
|
+
idempotencyKey?: string;
|
|
228
|
+
/**
|
|
229
|
+
* On `DUPLICATE_REQUEST_DETECTED` only. Re-send with this VALUE as your
|
|
230
|
+
* `idempotencyKey` to replay the original payout instead of making a second
|
|
231
|
+
* one. Set by the client at runtime and previously undeclared — so the
|
|
232
|
+
* recovery path both ERRORS.md and the README instruct you to take did not
|
|
233
|
+
* typecheck, on the guard whose entire purpose is preventing a double
|
|
234
|
+
* payment.
|
|
235
|
+
*/
|
|
236
|
+
originalIdempotencyKey?: string;
|
|
237
|
+
/** On `DUPLICATE_REQUEST_DETECTED` only. The payout the first request made. */
|
|
238
|
+
originalPayoutId?: string;
|
|
239
|
+
/**
|
|
240
|
+
* Send a SECOND real payment when an identical body was seen in the last 15
|
|
241
|
+
* minutes. Sets `X-Allow-Duplicate: true`. Used by the README's recovery
|
|
242
|
+
* example and previously undeclared.
|
|
243
|
+
*
|
|
244
|
+
* This is the branch that pays twice. To REPLAY the original instead, re-send
|
|
245
|
+
* with `idempotencyKey` set to the `originalIdempotencyKey` value from the
|
|
246
|
+
* 409 body.
|
|
247
|
+
*/
|
|
248
|
+
allowDuplicate?: boolean;
|
|
249
|
+
reference?: string;
|
|
250
|
+
purposeOfPayment?: string;
|
|
251
|
+
endUser?: EndUser;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export declare class PayoutsClient {
|
|
255
|
+
constructor(opts?: PayoutsClientOptions);
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Which world this key pays into, from its prefix. Assert on it in your own
|
|
259
|
+
* suite — `expect(avvio.mode).toBe('test')` before a test that sends is the
|
|
260
|
+
* cheapest guard there is against a live key reaching a payroll fixture.
|
|
261
|
+
*
|
|
262
|
+
* A key that is not recognisably `ak_test_` reports `'live'`, because the two
|
|
263
|
+
* wrong answers are not equally bad.
|
|
264
|
+
*/
|
|
265
|
+
readonly mode: 'test' | 'live';
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* The currencies you can pay out to, and what this routing can do.
|
|
269
|
+
*
|
|
270
|
+
* `capabilities` was missing from this declaration while the docs said
|
|
271
|
+
* "check `capabilities.exactOutput` on the corridors call" — so a TypeScript
|
|
272
|
+
* caller following the instruction had no typed path to the field and had to
|
|
273
|
+
* reach for an unsound cast.
|
|
274
|
+
*/
|
|
275
|
+
corridors(): Promise<{
|
|
276
|
+
corridors: Corridor[];
|
|
277
|
+
capabilities: { exactOutput: boolean; indicativePricing: boolean };
|
|
278
|
+
}>;
|
|
279
|
+
requirements(currency: string): Promise<Corridor>;
|
|
280
|
+
|
|
281
|
+
quote(args: {
|
|
282
|
+
amount: string;
|
|
283
|
+
to: string;
|
|
284
|
+
from?: string;
|
|
285
|
+
}): Promise<PreviewQuote>;
|
|
286
|
+
|
|
287
|
+
createBeneficiary(args: {
|
|
288
|
+
name: string;
|
|
289
|
+
currency: string;
|
|
290
|
+
details: Record<string, string>;
|
|
291
|
+
/** REQUIRED. The server rejects a create without it. */
|
|
292
|
+
email: string;
|
|
293
|
+
type?: 'individual' | 'business';
|
|
294
|
+
country?: string;
|
|
295
|
+
/** YOUR id for the person SENDING. Scopes the beneficiary to them. */
|
|
296
|
+
endUserId?: string;
|
|
297
|
+
/** YOUR id for this beneficiary. Makes a repeat create safe. */
|
|
298
|
+
externalId?: string;
|
|
299
|
+
idempotencyKey?: string;
|
|
300
|
+
}): Promise<Beneficiary>;
|
|
301
|
+
|
|
302
|
+
listBeneficiaries(args?: {
|
|
303
|
+
endUserId?: string;
|
|
304
|
+
}): Promise<{
|
|
305
|
+
/** 'organization' or the end user the list was scoped to. Undeclared previously. */
|
|
306
|
+
scope: string;
|
|
307
|
+
recipients: Beneficiary[];
|
|
308
|
+
/**
|
|
309
|
+
* This route is NOT paginated today — these never appear. Kept optional so
|
|
310
|
+
* adding pagination later is not a breaking change.
|
|
311
|
+
*/
|
|
312
|
+
hasMore?: boolean;
|
|
313
|
+
nextCursor?: string | null;
|
|
314
|
+
}>;
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Every event since a watermark, paged for you. At-least-once — `since` is
|
|
318
|
+
* inclusive, so a resumed run re-reads the row at your watermark. Dedupe on
|
|
319
|
+
* `id`.
|
|
320
|
+
*/
|
|
321
|
+
eachEvent(args?: {
|
|
322
|
+
since?: string;
|
|
323
|
+
limit?: number;
|
|
324
|
+
payoutId?: string;
|
|
325
|
+
}): AsyncIterableIterator<PayoutEvent>;
|
|
326
|
+
|
|
327
|
+
/** Every payout matching a filter, paged for you. */
|
|
328
|
+
eachPayout(args?: {
|
|
329
|
+
limit?: number;
|
|
330
|
+
status?: string;
|
|
331
|
+
endUserId?: string;
|
|
332
|
+
reference?: string;
|
|
333
|
+
updatedSince?: string;
|
|
334
|
+
}): AsyncIterableIterator<Payout>;
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* The raw escape hatch: any method, any path, with auth and error handling
|
|
338
|
+
* applied. For an endpoint the typed methods do not cover yet.
|
|
339
|
+
*
|
|
340
|
+
* Implemented since the beginning and undeclared until now, so a TypeScript
|
|
341
|
+
* user could not reach the one method that exists for reaching everything
|
|
342
|
+
* else.
|
|
343
|
+
*/
|
|
344
|
+
request(
|
|
345
|
+
method: string,
|
|
346
|
+
path: string,
|
|
347
|
+
opts?: {
|
|
348
|
+
body?: unknown;
|
|
349
|
+
query?: Record<string, unknown>;
|
|
350
|
+
idempotencyKey?: string;
|
|
351
|
+
headers?: Record<string, string>;
|
|
352
|
+
},
|
|
353
|
+
): Promise<unknown>;
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Stop a payout that has not been funded yet. Once funded it cannot be
|
|
357
|
+
* cancelled — you get `PAYOUT_NOT_CANCELABLE`, which is the honest answer.
|
|
358
|
+
*/
|
|
359
|
+
cancelPayout(
|
|
360
|
+
payoutId: string,
|
|
361
|
+
opts?: { idempotencyKey?: string },
|
|
362
|
+
): Promise<Payout>;
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* The two halves of `createPayout`, exposed for callers that need to show a
|
|
366
|
+
* binding quote before committing. Both are implemented and were undeclared,
|
|
367
|
+
* so TypeScript users could not reach them.
|
|
368
|
+
*/
|
|
369
|
+
pricePayout(args: {
|
|
370
|
+
amount: string;
|
|
371
|
+
destinationAccountId: string;
|
|
372
|
+
purposeOfPayment?: string;
|
|
373
|
+
// Deliberately loose: the snapshot is the rail's own quote object and its
|
|
374
|
+
// shape varies by routing. Pass it straight back to `send()`.
|
|
375
|
+
}): Promise<{ id: string; best_quote_id?: string; [k: string]: unknown }>;
|
|
376
|
+
|
|
377
|
+
send(args: {
|
|
378
|
+
snapshotId: string;
|
|
379
|
+
quoteId?: string;
|
|
380
|
+
endUser?: EndUser;
|
|
381
|
+
reference?: string;
|
|
382
|
+
idempotencyKey?: string;
|
|
383
|
+
}): Promise<Payout>;
|
|
384
|
+
|
|
385
|
+
/** Price and send in one call, with the drift guard applied. */
|
|
386
|
+
payout(args: PayoutArgs): Promise<Payout>;
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Everything that has happened to your payouts, in order.
|
|
390
|
+
*
|
|
391
|
+
* The reconciliation primitive. Events are written once and never change, so
|
|
392
|
+
* carrying `nextSince` gives you exactly what is new — including a bank return
|
|
393
|
+
* that lands days after you booked the payout as settled, which listing
|
|
394
|
+
* payouts (ordered by creation) can never surface.
|
|
395
|
+
*
|
|
396
|
+
* At-least-once: dedupe on `id`.
|
|
397
|
+
*/
|
|
398
|
+
listEvents(args?: {
|
|
399
|
+
/** The `nextSince` from your last page. */
|
|
400
|
+
since?: string;
|
|
401
|
+
limit?: number;
|
|
402
|
+
/** Everything that ever happened to one payout. */
|
|
403
|
+
payoutId?: string;
|
|
404
|
+
}): Promise<{
|
|
405
|
+
data: PayoutEvent[];
|
|
406
|
+
hasMore: boolean;
|
|
407
|
+
nextSince: string | null;
|
|
408
|
+
}>;
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* How to fund a payout that came back with `requiresFunding: true`. Your funds
|
|
412
|
+
* stay in your wallet until you move them.
|
|
413
|
+
*/
|
|
414
|
+
getFunding(payoutId: string): Promise<{
|
|
415
|
+
payoutId: string;
|
|
416
|
+
amount: string;
|
|
417
|
+
currency: string;
|
|
418
|
+
depositAddress: string;
|
|
419
|
+
network: string;
|
|
420
|
+
expiresAt: string;
|
|
421
|
+
/** Present when you can sign through this API instead of broadcasting. */
|
|
422
|
+
signableOperations?: unknown[];
|
|
423
|
+
instructions: string;
|
|
424
|
+
}>;
|
|
425
|
+
|
|
426
|
+
/** Proof you sent the funds: a hash you broadcast, or operations you signed. */
|
|
427
|
+
confirmFunding(
|
|
428
|
+
payoutId: string,
|
|
429
|
+
proof: {
|
|
430
|
+
transactionHash?: string;
|
|
431
|
+
signedOperations?: unknown[];
|
|
432
|
+
tamperProofSignature?: string;
|
|
433
|
+
idempotencyKey?: string;
|
|
434
|
+
},
|
|
435
|
+
): Promise<Payout>;
|
|
436
|
+
|
|
437
|
+
/** Always live. More authoritative than a webhook you may have missed. */
|
|
438
|
+
getPayout(payoutId: string): Promise<Payout>;
|
|
439
|
+
/**
|
|
440
|
+
* A PAGE of payouts, not an array.
|
|
441
|
+
*
|
|
442
|
+
* This was declared as `Payout[]`, so code written straight off the type —
|
|
443
|
+
* `payouts.map(...)`, `payouts.length` — threw a TypeError on the first call.
|
|
444
|
+
* In a reconciliation job wrapped in try/catch that is a silently skipped
|
|
445
|
+
* cycle rather than an alert.
|
|
446
|
+
*/
|
|
447
|
+
listPayouts(args?: {
|
|
448
|
+
limit?: number;
|
|
449
|
+
cursor?: string;
|
|
450
|
+
status?: string;
|
|
451
|
+
endUserId?: string;
|
|
452
|
+
reference?: string;
|
|
453
|
+
/**
|
|
454
|
+
* **The change feed.** Payouts whose state changed at or after this time,
|
|
455
|
+
* oldest-changed first.
|
|
456
|
+
*
|
|
457
|
+
* Ordinary listing is newest-first by CREATION, which by construction can
|
|
458
|
+
* never tell you an OLD payout changed — and `completed → failed` on a bank
|
|
459
|
+
* return, days later, is exactly the change a ledger cannot afford to miss.
|
|
460
|
+
* Page forward, carry the highest `updatedAt` you have seen as your
|
|
461
|
+
* watermark, and you observe every revision at least once.
|
|
462
|
+
*
|
|
463
|
+
* `updatedSince` is INCLUSIVE, so resuming re-reads the row at your
|
|
464
|
+
* watermark. Dedupe on payoutId + updatedAt.
|
|
465
|
+
*/
|
|
466
|
+
updatedSince?: string;
|
|
467
|
+
}): Promise<{ data: Payout[]; hasMore: boolean; nextCursor: string | null }>;
|
|
468
|
+
|
|
469
|
+
fundingAccounts(): Promise<unknown>;
|
|
470
|
+
|
|
471
|
+
/** What you can currently send. */
|
|
472
|
+
balance(): Promise<{ currency: string; amount: string }>;
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Why the balance is what it is: every movement with the running balance
|
|
476
|
+
* after it. Reconcile against this rather than trusting a single number.
|
|
477
|
+
*/
|
|
478
|
+
balanceHistory(limit?: number): Promise<{
|
|
479
|
+
currency: string;
|
|
480
|
+
balance: string;
|
|
481
|
+
entries: {
|
|
482
|
+
id: string;
|
|
483
|
+
/**
|
|
484
|
+
* `reversal` is a returned payout crediting the balance back. It is a
|
|
485
|
+
* SEPARATE appended entry, not a rewrite of the original debit — an entry
|
|
486
|
+
* already emitted never changes.
|
|
487
|
+
*/
|
|
488
|
+
type: 'funding' | 'payout' | 'reversal';
|
|
489
|
+
/**
|
|
490
|
+
* What this ENTRY records, frozen at the moment it was written — not the
|
|
491
|
+
* payout's current status, which would make a historical row mutate.
|
|
492
|
+
* `DEBITED` money left, `RETURNED` money came back, `COMPLETED` funding
|
|
493
|
+
* landed. For a payout's live fate, read `getPayout()`.
|
|
494
|
+
*/
|
|
495
|
+
status: 'DEBITED' | 'RETURNED' | 'COMPLETED' | (string & {});
|
|
496
|
+
amount: string;
|
|
497
|
+
balanceAfter: string;
|
|
498
|
+
reference?: string;
|
|
499
|
+
at: string;
|
|
500
|
+
}[];
|
|
501
|
+
}>;
|
|
502
|
+
|
|
503
|
+
/** Credit a sandbox balance. Test keys only. */
|
|
504
|
+
fund(amount?: string, idempotencyKey?: string): Promise<{ balance: string }>;
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Mint a one-time link for the person being paid, so they enter their own
|
|
508
|
+
* bank details and you never hold them.
|
|
509
|
+
*
|
|
510
|
+
* The token the recipient's page needs is the part of `url` after `/l/`.
|
|
511
|
+
*/
|
|
512
|
+
createPayoutLink(args: {
|
|
513
|
+
amount: string;
|
|
514
|
+
/** e.g. 'MXN'. `to` is accepted as an alias. */
|
|
515
|
+
destinationCurrency?: string;
|
|
516
|
+
to?: string;
|
|
517
|
+
endUserId: string;
|
|
518
|
+
reference?: string;
|
|
519
|
+
/** Defaults to 60. Capped at 7 days. */
|
|
520
|
+
expiresInMinutes?: number;
|
|
521
|
+
idempotencyKey?: string;
|
|
522
|
+
}): Promise<{
|
|
523
|
+
payoutLinkId: string;
|
|
524
|
+
url: string;
|
|
525
|
+
expiresAt: string;
|
|
526
|
+
status: string;
|
|
527
|
+
}>;
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Register a sandbox webhook endpoint. The `secret` is returned ONCE and is
|
|
531
|
+
* not retrievable afterwards. Test keys only.
|
|
532
|
+
*/
|
|
533
|
+
createWebhookEndpoint(args: {
|
|
534
|
+
url: string;
|
|
535
|
+
events?: string[];
|
|
536
|
+
idempotencyKey?: string;
|
|
537
|
+
}): Promise<{
|
|
538
|
+
id: string;
|
|
539
|
+
url: string;
|
|
540
|
+
events: string[];
|
|
541
|
+
secret: string;
|
|
542
|
+
warning: string;
|
|
543
|
+
}>;
|
|
544
|
+
|
|
545
|
+
/** What we sent, what came back, and what we retried. */
|
|
546
|
+
webhookDeliveries(endpointId: string): Promise<unknown>;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
export declare class WebhookVerificationError extends Error {
|
|
550
|
+
readonly name: 'WebhookVerificationError';
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* A deterministic idempotency key, formatted as a UUID.
|
|
555
|
+
*
|
|
556
|
+
* stableKey(orgId, payrollRunId, employeeId)
|
|
557
|
+
*
|
|
558
|
+
* The same parts always produce the same key, so a job that crashes and
|
|
559
|
+
* requeues retries into a REPLAY rather than sending a second payment. The
|
|
560
|
+
* generated `randomUUID()` a call gets by default cannot do that: it is new on
|
|
561
|
+
* every attempt, which is precisely the restart the retry docs are about.
|
|
562
|
+
*
|
|
563
|
+
* Parts are NUL-separated, so `('a','bc')` and `('ab','c')` are different keys.
|
|
564
|
+
* An empty or missing part throws rather than silently collapsing two people's
|
|
565
|
+
* wages onto one key.
|
|
566
|
+
*/
|
|
567
|
+
export declare function stableKey(...parts: (string | number)[]): string;
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* The payout as it appears INSIDE A WEBHOOK.
|
|
571
|
+
*
|
|
572
|
+
* Deliberately not `Payout`. The webhook body is flat — `sourceAmount` is a
|
|
573
|
+
* decimal string beside a separate `sourceCurrency` — where the REST payout
|
|
574
|
+
* nests them as `Money` objects. Declaring `data: Payout` meant that code
|
|
575
|
+
* written off the type (`event.data.sourceAmount.amount`) threw a TypeError on
|
|
576
|
+
* the first real delivery.
|
|
577
|
+
*
|
|
578
|
+
* That is the same mistake `listPayouts` made, shipped a second time in the one
|
|
579
|
+
* place the docs tell you to build first. The two shapes are now separate types
|
|
580
|
+
* so they cannot be confused, and `scripts/sdk-contract.ts` asserts this one
|
|
581
|
+
* against a real delivered event.
|
|
582
|
+
*/
|
|
583
|
+
export interface PayoutEvent {
|
|
584
|
+
/** Stable. Dedupe on this — the feed is at-least-once. */
|
|
585
|
+
id: string;
|
|
586
|
+
/**
|
|
587
|
+
* The cursor, as a decimal STRING. A 64-bit sequence past 2^53 is not
|
|
588
|
+
* representable as a JSON number, and losing precision on a cursor is
|
|
589
|
+
* unrecoverable.
|
|
590
|
+
*/
|
|
591
|
+
sequence: string;
|
|
592
|
+
/**
|
|
593
|
+
* `payout.returned` is its OWN type, not a flavour of `payout.failed` — it is
|
|
594
|
+
* the one event that reverses something you already booked. New types are
|
|
595
|
+
* added without a major version; ignore ones you do not handle.
|
|
596
|
+
*/
|
|
597
|
+
type:
|
|
598
|
+
| 'payout.pending'
|
|
599
|
+
| 'payout.processing'
|
|
600
|
+
| 'payout.completed'
|
|
601
|
+
| 'payout.failed'
|
|
602
|
+
| 'payout.returned'
|
|
603
|
+
| (string & {});
|
|
604
|
+
payoutId: string;
|
|
605
|
+
status: PayoutStatus;
|
|
606
|
+
failureCode?: PayoutFailureCode;
|
|
607
|
+
fundsReturned?: boolean;
|
|
608
|
+
createdAt: string;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
export interface WebhookPayout {
|
|
612
|
+
payoutId: string;
|
|
613
|
+
status: PayoutStatus;
|
|
614
|
+
stage?: PayoutStage;
|
|
615
|
+
failureCode?: PayoutFailureCode;
|
|
616
|
+
/** Absent when not yet established. Absent is NOT the same as false. */
|
|
617
|
+
fundsReturned?: boolean;
|
|
618
|
+
/** Decimal strings, not `Money`. Currency is the adjacent field. */
|
|
619
|
+
sourceCurrency: string | null;
|
|
620
|
+
sourceAmount: string | null;
|
|
621
|
+
destinationCurrency: string | null;
|
|
622
|
+
destinationAmount: string | null;
|
|
623
|
+
destinationAccountId: string | null;
|
|
624
|
+
rate: string | null;
|
|
625
|
+
endUser: EndUser | null;
|
|
626
|
+
reference?: string | null;
|
|
627
|
+
createdAt: string;
|
|
628
|
+
completedAt: string | null;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
export interface WebhookEvent {
|
|
632
|
+
/** New types are added without a major version — return 2xx for unknown ones. */
|
|
633
|
+
/**
|
|
634
|
+
* Every type actually delivered, confirmed by capturing raw bodies — this
|
|
635
|
+
* omitted `payout.returned`, `payout.canceled` and `payout.processing`, all
|
|
636
|
+
* three of which arrive. The `(string & {})` escape made the omission
|
|
637
|
+
* invisible: an exhaustive switch compiled with no `payout.returned` case,
|
|
638
|
+
* and a bank return was silently ignored — the exact failure the separate
|
|
639
|
+
* type exists to prevent.
|
|
640
|
+
*/
|
|
641
|
+
type:
|
|
642
|
+
| 'payout.pending'
|
|
643
|
+
| 'payout.processing'
|
|
644
|
+
| 'payout.completed'
|
|
645
|
+
| 'payout.failed'
|
|
646
|
+
| 'payout.returned'
|
|
647
|
+
| 'payout.canceled'
|
|
648
|
+
| (string & {});
|
|
649
|
+
data: WebhookPayout;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Verify a delivery and return its parsed body.
|
|
654
|
+
*
|
|
655
|
+
* `body` must be the RAW bytes. Re-serializing a parsed object does not
|
|
656
|
+
* reproduce the same bytes, and one reordered key fails every signature.
|
|
657
|
+
*/
|
|
658
|
+
export declare function verifyWebhook(args: {
|
|
659
|
+
body: string | Buffer;
|
|
660
|
+
headers: Record<string, string | string[] | undefined>;
|
|
661
|
+
secret: string;
|
|
662
|
+
toleranceSeconds?: number;
|
|
663
|
+
}): WebhookEvent;
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* A (req, res) handler that verifies the signature from the RAW request stream.
|
|
667
|
+
*
|
|
668
|
+
* The failure `verifyWebhook` alone cannot prevent is upstream of it: a body
|
|
669
|
+
* parser consumes and discards the raw bytes before your handler runs, and the
|
|
670
|
+
* signature is over those bytes. This reads the stream itself, so it is correct
|
|
671
|
+
* whichever middleware is mounted and in whatever order.
|
|
672
|
+
*
|
|
673
|
+
* Framework-agnostic: Express, Fastify's raw handler, or node:http.
|
|
674
|
+
*/
|
|
675
|
+
export declare function createWebhookHandler(opts: {
|
|
676
|
+
secret: string;
|
|
677
|
+
onEvent: (event: WebhookEvent) => void | Promise<void>;
|
|
678
|
+
/** Reject timestamps outside this window. Defaults to 300. */
|
|
679
|
+
toleranceSeconds?: number;
|
|
680
|
+
}): (req: any, res: any) => Promise<void>;
|