@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/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>;