Node SDK
@avvio/payments v0.1.0 — a Node client, the CLI above and an MCP server in one package with zero runtime dependencies. Not a boast: this package holds a credential that moves money, transitive npm compromise is the realistic path to it, and the shortest answer to "what is in this dependency tree" is nothing. Node 18 or newer.
npm i @avvio/paymentsconst { PayoutsClient } = require('@avvio/payments');
const avvio = new PayoutsClient(); // reads AVVIO_API_KEY and AVVIO_ORG_IDPaying someone#
avvio-payments beneficiary create \
--name "María González" --email maria@example.com \
--currency MXN --country MX \
--end-user employee_42 --external-id emp42_maria \
--field clabeNumber=012345678901234567curl -s -X POST "$AVVIO_BASE_URL/recipients/$AVVIO_ORG_ID" \
-H "x-api-key: $AVVIO_API_KEY" \
-H "idempotency-key: $(uuidgen)" \
-H "content-type: application/json" \
-d '{
"type": "individual",
"name": "María González",
"email": "maria@example.com",
"country": "MX",
"endUserId": "employee_42",
"method": {
"kind": "fiat",
"currency": "MXN",
"recipientDetails": {
"clabeNumber": "012345678901234567"
}
}
}'const beneficiary = await avvio.createBeneficiary({
name: 'María González',
email: 'maria@example.com',
country: 'MX',
currency: 'MXN',
endUserId: 'employee_42', // YOUR id for the person sending
externalId: 'emp42_maria', // makes a repeat create safe
details: { clabeNumber: '012345678901234567' },
});avvio-payments pay --amount 200.00 \
--to <destinationAccountId> \
--expect 3384.65 \
--end-user employee_42 --reference ZZ-2026-0042curl -s -X POST "$AVVIO_BASE_URL/payments/organizations/$AVVIO_ORG_ID/payouts" \
-H "x-api-key: $AVVIO_API_KEY" \
-H "idempotency-key: $(uuidgen)" \
-H "content-type: application/json" \
-d '{
"amount": "200.00",
"destinationAccountId": "<destinationAccountId>",
"expectDestination": "3384.65",
"maxDriftBps": 200,
"endUser": {
"id": "employee_42"
}
}'const payout = await avvio.payout({
amount: '200.00',
destinationAccountId: acct,
expectDestination: quote.destinationAmount.amount, // 3384.65
endUser: { id: 'employee_42' },
reference: 'ZZ-2026-0042',
});Every method#
25 methods, read from the index.d.ts that ships with the package. eachEvent and eachPayout are async iterators that page for you.
constructor(opts?: PayoutsClientOptions);corridors(): Promise<{
corridors: Corridor[];
capabilities: { exactOutput: boolean; indicativePricing: boolean };
}>;The currencies you can pay out to, and what this routing can do.
capabilities was missing from this declaration while the docs said "check capabilities.exactOutput on the corridors call" — so a TypeScript caller following the instruction had no typed path to the field and had to reach for an unsound cast.requirements(currency: string): Promise<Corridor>;quote(args: {
amount: string;
to: string;
from?: string;
}): Promise<PreviewQuote>;createBeneficiary(args: {
name: string;
currency: string;
details: Record<string, string>;
/** REQUIRED. The server rejects a create without it. */
email: string;
type?: 'individual' | 'business';
country?: string;
/** YOUR id for the person SENDING. Scopes the beneficiary to them. */
endUserId?: string;
/** YOUR id for this beneficiary. Makes a repeat create safe. */
externalId?: string;
idempotencyKey?: string;
}): Promise<Beneficiary>;listBeneficiaries(args?: {
endUserId?: string;
}): Promise<{
/** 'organization' or the end user the list was scoped to. Undeclared previously. */
scope: string;
recipients: Beneficiary[];
/**
* This route is NOT paginated today — these never appear. Kept optional so
* adding pagination later is not a breaking change.
*/
hasMore?: boolean;
nextCursor?: string | null;
}>;eachEvent(args?: {
since?: string;
limit?: number;
payoutId?: string;
}): AsyncIterableIterator<PayoutEvent>;Every event since a watermark, paged for you. At-least-once —
since is inclusive, so a resumed run re-reads the row at your watermark. Dedupe on id.eachPayout(args?: {
limit?: number;
status?: string;
endUserId?: string;
reference?: string;
updatedSince?: string;
}): AsyncIterableIterator<Payout>;Every payout matching a filter, paged for you. */
request(
method: string,
path: string,
opts?: {
body?: unknown;
query?: Record<string, unknown>;
idempotencyKey?: string;
headers?: Record<string, string>;
},
): Promise<unknown>;The raw escape hatch: any method, any path, with auth and error handling applied. For an endpoint the typed methods do not cover yet. Implemented since the beginning and undeclared until now, so a TypeScript user could not reach the one method that exists for reaching everything else.
cancelPayout(
payoutId: string,
opts?: { idempotencyKey?: string },
): Promise<Payout>;Stop a payout that has not been funded yet. Once funded it cannot be cancelled — you get
PAYOUT_NOT_CANCELABLE, which is the honest answer.pricePayout(args: {
amount: string;
destinationAccountId: string;
purposeOfPayment?: string;
// Deliberately loose: the snapshot is the rail's own quote object and its
// shape varies by routing. Pass it straight back to `send()`.
}): Promise<{ id: string; best_quote_id?: string; [k: string]: unknown }>;The two halves of
createPayout, exposed for callers that need to show a binding quote before committing. Both are implemented and were undeclared, so TypeScript users could not reach them.send(args: {
snapshotId: string;
quoteId?: string;
endUser?: EndUser;
reference?: string;
idempotencyKey?: string;
}): Promise<Payout>;payout(args: PayoutArgs): Promise<Payout>;Price and send in one call, with the drift guard applied. */
listEvents(args?: {
/** The `nextSince` from your last page. */
since?: string;
limit?: number;
/** Everything that ever happened to one payout. */
payoutId?: string;
}): Promise<{
data: PayoutEvent[];
hasMore: boolean;
nextSince: string | null;
}>;Everything that has happened to your payouts, in order. The reconciliation primitive. Events are written once and never change, so carrying
nextSince gives you exactly what is new — including a bank return that lands days after you booked the payout as settled, which listing payouts (ordered by creation) can never surface. At-least-once: dedupe on id.getFunding(payoutId: string): Promise<{
payoutId: string;
amount: string;
currency: string;
depositAddress: string;
network: string;
expiresAt: string;
/** Present when you can sign through this API instead of broadcasting. */
signableOperations?: unknown[];
instructions: string;
}>;How to fund a payout that came back with
requiresFunding: true. Your funds stay in your wallet until you move them.confirmFunding(
payoutId: string,
proof: {
transactionHash?: string;
signedOperations?: unknown[];
tamperProofSignature?: string;
idempotencyKey?: string;
},
): Promise<Payout>;Proof you sent the funds: a hash you broadcast, or operations you signed. */
getPayout(payoutId: string): Promise<Payout>;Always live. More authoritative than a webhook you may have missed. */
listPayouts(args?: {
limit?: number;
cursor?: string;
status?: string;
endUserId?: string;
reference?: string;
/**
* **The change feed.** Payouts whose state changed at or after this time,
* oldest-changed first.
*
* Ordinary listing is newest-first by CREATION, which by construction can
* never tell you an OLD payout changed — and `completed → failed` on a bank
* return, days later, is exactly the change a ledger cannot afford to miss.
* Page forward, carry the highest `updatedAt` you have seen as your
* watermark, and you observe every revision at least once.
*
* `updatedSince` is INCLUSIVE, so resuming re-reads the row at your
* watermark. Dedupe on payoutId + updatedAt.
*/
updatedSince?: string;
}): Promise<{ data: Payout[]; hasMore: boolean; nextCursor: string | null }>;A PAGE of payouts, not an array. This was declared as
Payout[], so code written straight off the type — payouts.map(...), payouts.length — threw a TypeError on the first call. In a reconciliation job wrapped in try/catch that is a silently skipped cycle rather than an alert.fundingAccounts(): Promise<unknown>;balance(): Promise<{ currency: string; amount: string }>;What you can currently send. */
balanceHistory(limit?: number): Promise<{
currency: string;
balance: string;
entries: {
id: string;
/**
* `reversal` is a returned payout crediting the balance back. It is a
* SEPARATE appended entry, not a rewrite of the original debit — an entry
* already emitted never changes.
*/
type: 'funding' | 'payout' | 'reversal';
/**
* What this ENTRY records, frozen at the moment it was written — not the
* payout's current status, which would make a historical row mutate.
* `DEBITED` money left, `RETURNED` money came back, `COMPLETED` funding
* landed. For a payout's live fate, read `getPayout()`.
*/
status: 'DEBITED' | 'RETURNED' | 'COMPLETED' | (string & {});
amount: string;
balanceAfter: string;
reference?: string;
at: string;
}[];
}>;Why the balance is what it is: every movement with the running balance after it. Reconcile against this rather than trusting a single number.
fund(amount?: string, idempotencyKey?: string): Promise<{ balance: string }>;Credit a sandbox balance. Test keys only. */
createPayoutLink(args: {
amount: string;
/** e.g. 'MXN'. `to` is accepted as an alias. */
destinationCurrency?: string;
to?: string;
endUserId: string;
reference?: string;
/** Defaults to 60. Capped at 7 days. */
expiresInMinutes?: number;
idempotencyKey?: string;
}): Promise<{
payoutLinkId: string;
url: string;
expiresAt: string;
status: string;
}>;Mint a one-time link for the person being paid, so they enter their own bank details and you never hold them. The token the recipient's page needs is the part of
url after /l/.createWebhookEndpoint(args: {
url: string;
events?: string[];
idempotencyKey?: string;
}): Promise<{
id: string;
url: string;
events: string[];
secret: string;
warning: string;
}>;Register a sandbox webhook endpoint. The
secret is returned ONCE and is not retrievable afterwards. Test keys only.webhookDeliveries(endpointId: string): Promise<unknown>;What we sent, what came back, and what we retried. */