Skip to content

@avvio/payments v0.8.1: a Node client, the CLI and an MCP server in one package with zero runtime dependencies. Node 18 or newer.

npm i @avvio/payments
const { PayoutsClient } = require('@avvio/payments');
const avvio = new PayoutsClient(); // also reads AVVIO_API_KEY and AVVIO_ORG_ID
POST /recipients/{orgId}
const beneficiary = await avvio.createBeneficiary({
name: 'María González',
email: '[email protected]', // optional contact detail
country: 'MX',
currency: 'MXN',
endUserId: 'customer_42', // your id for the person sending
externalId: 'cust42_maria', // makes a repeat create safe
details: { clabeNumber: '012180000080004471' },
});
const destinationAccountId = beneficiary.method.destinationAccountId; // you pay this
POST /payments/organizations/{orgId}/payouts
const idempotencyKey = crypto.randomUUID(); // persist it with the order so a retry reuses it
const payout = await avvio.payout({
amount: '200.00',
destinationAccountId,
expectDestination: quote.destinationAmount.amount, // 3384.65
endUser: { id: 'customer_42' },
reference: 'ZZ-2026-0042',
idempotencyKey,
});

57 methods, parsed from the index.d.ts shipping with the package. eachEvent, eachPayout, eachAuditEvent and eachBalanceTransaction are async iterators that handle pagination for you.

constructor(opts?: PayoutsClientOptions);

The currencies you can pay out to, and what this routing can do. capabilities.exactOutput says whether the locking amountLeg modes work.

corridors(currency?: string): Promise<{
corridors: Corridor[];
capabilities: { exactOutput: boolean; indicativePricing: boolean };
}>;
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>;
/** Optional contact address, kept for your records. Stored as null when omitted. */
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;
/** 1 to 100; defaults to 50. */
limit?: number;
/** The `nextCursor` from the previous page, unchanged. */
cursor?: string;
}): Promise<{
/** 'organization' or the end user the list was scoped to. */
scope: string;
recipients: Beneficiary[];
/** More rows follow; pass `nextCursor` back as `cursor`. */
hasMore?: boolean;
nextCursor?: string | null;
}>;

One beneficiary, by the id we returned. NOT_FOUND if it is not yours.

getBeneficiary(recipientId: string): Promise<Beneficiary>;

One beneficiary, by your id for them: the same externalId that makes creation idempotent. Unique per organization, so this is exactly one beneficiary or a NOT_FOUND, rather than paging your whole book.

getBeneficiaryByExternalId(externalId: string): Promise<Beneficiary>;

Correct a beneficiary’s own details. Only the fields you pass are changed. Bank details cannot be edited. The rail validated that account, and swapping it underneath would send the next payout somewhere you never registered. A wrong account is a new payment method, and the old one is deleted with deleteBeneficiaryMethod.

updateBeneficiary(
recipientId: string,
patch: {
type?: 'individual' | 'business';
name?: string;
email?: string;
phone?: string;
/** ISO-3166 alpha-2. */
country?: string;
},
opts?: { idempotencyKey?: string },
): Promise<Beneficiary>;

Remove a beneficiary and every payment method on it. Payouts already sent are history, not references; this cancels nothing in flight.

deleteBeneficiary(
recipientId: string,
opts?: { idempotencyKey?: string },
): Promise<{ success: true }>;

Remove one way of paying a beneficiary, such as an account that closed, or one entered wrong. Returns the beneficiary without it; the destinationAccountId that method carried stops being payable.

deleteBeneficiaryMethod(
recipientId: string,
methodId: string,
opts?: { idempotencyKey?: string },
): Promise<Beneficiary>;

The full account details behind one payment method. Lists carry only last4. Deliberately loose: the fields are the corridor’s own, so they differ by rail exactly as requirements() does.

getBeneficiaryMethodDetails(
recipientId: string,
methodId: string,
): Promise<Record<string, unknown>>;

The stated payment reasons this organization may use: the vocabulary purposeOfPayment on a payout, and paymentReason on a quote you accept, are validated against. Read it rather than guessing: a rejected value is a 400 on a payout you have already promised somebody.

paymentReasons(): Promise<{ payment_reasons: Record<string, unknown>[] }>;

Every event after a watermark, paged for you. since is exclusive, so resuming from a saved nextSince does not re-read it; still at-least-once (a crash before you save the watermark replays events). Dedupe on id.

eachEvent(args?: {
since?: string;
limit?: number;
payoutId?: string;
/** Event types to include; a string (`'payout.completed,payout.returned'`) or an array. */
type?: PayoutEventType | PayoutEventType[] | string;
}): AsyncIterableIterator<PayoutEvent>;

Every audit event matching a filter, newest first, paged for you.

eachAuditEvent(
args?: AuditEventFilters & {
limit?: number;
},
): AsyncIterableIterator<AuditEvent>;

Every payout matching a filter, paged for you.

eachPayout(args?: {
limit?: number;
status?: string;
endUserId?: string;
reference?: string;
updatedSince?: string;
}): AsyncIterableIterator<Payout>;

The raw escape hatch: any method, any path, with auth and error handling applied. For an endpoint the typed methods do not cover yet.

request(
method: string,
path: string,
opts?: {
body?: unknown;
query?: Record<string, unknown>;
idempotencyKey?: string;
headers?: Record<string, string>;
},
): Promise<unknown>;

Stop a payout that has not been funded yet. Once funded it cannot be canceled, and you get PAYOUT_NOT_CANCELABLE.

cancelPayout(
payoutId: string,
opts?: { idempotencyKey?: string },
): Promise<Payout>;

The two halves of createPayout, exposed for callers that need to show a binding quote before committing.

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 }>;
send(args: {
snapshotId: string;
quoteId?: string;
endUser?: EndUser;
reference?: string;
idempotencyKey?: string;
}): Promise<Payout>;

Price and send in one call, with the drift guard applied.

payout(args: PayoutArgs): Promise<Payout>;

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.

listEvents(args?: {
/** The `nextSince` from your last page. */
since?: string;
limit?: number;
/** Everything that ever happened to one payout. */
payoutId?: string;
/**
* Event types to include; a string (`'payout.completed,payout.returned'`)
* or an array. Unknown values are a 400, never an empty page.
*/
type?: PayoutEventType | PayoutEventType[] | string;
}): Promise<{
data: PayoutEvent[];
hasMore: boolean;
nextSince: string | null;
}>;

Payouts and batch runs waiting on your approvers. payout() and confirmPayoutBatch() answer 202 with a PendingApproval when the organization’s policy holds them; this is that queue. Approving and rejecting are dashboard actions. A key cannot take them, so there is no method.

listApprovals(args?: {
status?: PayoutApprovalStatus;
/** 1–100, default 50. */
limit?: number;
}): Promise<{ data: PayoutApproval[] }>;

One approval, by the approvalId a 202 gave you.

getApproval(approvalId: string): Promise<PayoutApproval>;

A page of audit events, newest first: who did what, from where, with which credential. id is the cursor. A read-only key may read this.

listAuditEvents(
args?: AuditEventFilters & {
/** 1–100, default 50. */
limit?: number;
/** A `nextCursor` we issued: rows strictly older than it. */
cursor?: string;
},
): Promise<{
data: AuditEvent[];
hasMore: boolean;
nextCursor: string | null;
}>;

How to fund a payout that came back with requiresFunding: true. Your funds stay in your wallet until you move them. A pure read; poll it freely.

getFunding(payoutId: string): Promise<{
payoutId: string;
amount: string;
currency: string;
depositAddress: string;
network: string;
/**
* Normally `null`. There is no countdown on the deposit address. The rail
* decides when an unfunded payout is over and reports that as the payout's
* own status, so read `getPayout()` before sending against instructions you
* fetched a while ago; do not build a timer on it.
*/
expiresAt: string | null;
instructions: string;
}>;

Proof you sent the funds: the transaction hash you broadcast.

confirmFunding(
payoutId: string,
proof: {
transactionHash: string;
idempotencyKey?: string;
},
): Promise<Payout>;

Always live. More authoritative than a webhook you may have missed.

getPayout(payoutId: string): Promise<Payout>;

A page of payouts, not an array: read the rows from the page and follow its cursor.

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 }>;

Submit up to 1,000 payouts as one run. 202 means received, not paid: every line is validated first (nothing priced, nothing debited), then the valid lines become ordinary payouts, each with its own payoutId.

createPayoutBatch(args: PayoutBatchArgs): Promise<PayoutBatch>;

A page of batches, newest first, not an array.

listPayoutBatches(args?: {
status?: PayoutBatchStatus;
/** Exact match on your own run id. */
externalReferenceId?: string;
/** 1–100, default 50. */
limit?: number;
/** A `nextCursor` we issued. Anything else is a 400, never an empty page. */
cursor?: string;
}): Promise<{
data: PayoutBatch[];
hasMore: boolean;
nextCursor: string | null;
}>;

One run, with its counts. Authoritative for the batch, not the payouts.

getPayoutBatch(batchId: string): Promise<PayoutBatch>;

The lines of a run, instruction echoed back verbatim. status: 'invalid' is the review screen after awaiting_confirmation; 'created' joins the run to the payout ledger. The CSV export (?format=csv) is a plain file download; this method returns the JSON page.

listPayoutBatchItems(
batchId: string,
args?: {
status?: PayoutBatchItemStatus;
/** 1–1,000, default 100. */
limit?: number;
/** The `nextCursor` from your previous page: the last line index. */
cursor?: string;
},
): Promise<{
data: PayoutBatchItem[];
hasMore: boolean;
nextCursor: string | null;
}>;

Proceed with the valid lines of a held run. Only legal from awaiting_confirmation; anything else is PAYOUT_BATCH_NOT_CONFIRMABLE.

confirmPayoutBatch(
batchId: string,
opts?: { idempotencyKey?: string },
): Promise<PayoutBatch>;

Stop a run before any payout exists. Once creation begins the run is committed and this is PAYOUT_BATCH_NOT_CANCELABLE. A payout the run created can be canceled with cancelPayout only while it waits on your own funding.

cancelPayoutBatch(
batchId: string,
opts?: { idempotencyKey?: string },
): Promise<PayoutBatch>;
fundingAccounts(): Promise<unknown>;

What your organization is bound by, read live. Read it first: caps, approval threshold, features, rate limits, idempotency windows and the currencies that need a purposeOfPayment.

getPolicy(): Promise<Policy>;

Everything a payout can draw on (cached up to 60 s): what the payment network holds for you (provider) plus the USD stablecoins in your own wallet (wallet, per chain, at face value), summed per currency into balances with USD as the amount headline. Size payouts against that, knowing wallet figures are face value before the network and conversion cost of moving them onto the settling rail. A payout for exactly amount can fail to fund when the money must be gathered from several chains first. ledger is our own append-only record of the network-held part, with what holds currently reserve. Never summed with the others; a difference from provider is what listBalanceTransactions() explains. unavailable names any source that failed to read: when non-empty the figures are a floor, not the balance, so retry before concluding you cannot fund a payout.

balance(): Promise<{
currency: string;
amount: string;
balances: Money[];
provider: Money[];
/** Always empty in sandbox. */
wallet: WalletBalance[];
/** Sources that failed to read; non-empty results are not cached, so retry. */
unavailable: Array<'network' | 'wallet'>;
ledger: LedgerBalance[];
}>;

A page of balance transactions, newest first, not an array. Every change to what you can spend, each with balanceAfter; id is the cursor and the dedupe key. Served on every environment; an empty page means no rows yet, not an error.

listBalanceTransactions(
args?: BalanceTransactionFilters & {
/** 1–100, default 100. */
limit?: number;
/** A `nextCursor` we issued: rows strictly older than it. */
cursor?: string;
},
): Promise<{
data: BalanceTransaction[];
hasMore: boolean;
nextCursor: string | null;
}>;

Every balance transaction matching a filter, newest first, paged for you.

eachBalanceTransaction(
args?: BalanceTransactionFilters & {
limit?: number;
},
): AsyncIterableIterator<BalanceTransaction>;

Credit a sandbox balance. Test keys only.

fund(amount?: string, idempotencyKey?: string): Promise<{ balance: 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/.

createPayoutLink(args: {
amount: string;
/** e.g. 'MXN'. `to` is accepted as an alias. */
destinationCurrency?: string;
to?: string;
endUserId: string;
/** The sender (your end user). `email` is where the Reg E receipt goes. */
endUser?: { name?: string; email?: string };
reference?: string;
/** Defaults to 60. Capped at 7 days. */
expiresInMinutes?: number;
idempotencyKey?: string;
}): Promise<{
payoutLinkId: string;
url: string;
expiresAt: string;
status: string;
}>;

Create a checkout link; with publish: true the response carries shareUrl. An idempotency key is minted when you do not pass one (the server requires it on every checkout write). A refused publish does not reject: the promise resolves with the kept draft, status: 'draft' and publishError set. Branch on link.status !== 'sent'.

createCheckoutLink(args: CreateCheckoutLinkArgs): Promise<CheckoutLink>;
getCheckoutLink(linkId: string): Promise<CheckoutLink>;
listCheckoutLinks(args?: {
status?: CheckoutLinkStatus;
/** `api`: made with an API key. `dashboard`: made by a person. Omit for both. */
source?: CheckoutLinkSource;
limit?: number;
cursor?: string;
}): Promise<{ items: CheckoutLink[]; nextCursor?: string }>;

Edit a draft, or move a live link’s deadline. A sent link accepts expiresAt alone (an instant, or null to clear); anything else on a live link is a 400.

updateCheckoutLink(
linkId: string,
args: UpdateCheckoutLinkArgs,
): Promise<CheckoutLink>;

Permanent: a paused link cannot be republished.

pauseCheckoutLink(linkId: string): Promise<CheckoutLink>;

Newest first. Base units with decimals; the webhook uses decimal strings.

listCheckoutPayments(
linkId: string,
args?: { limit?: number; cursor?: string },
): Promise<{ items: CheckoutPayment[]; nextCursor?: string }>;

Moves money: refunds a card payment, whole (omit amount) or in part. The key needs the refunds scope. Retry a timeout with the same idempotencyKey. Resolves with the payment after the refund and the refund just made.

refundCheckoutPayment(
paymentId: string,
args?: RefundCheckoutPaymentArgs,
): Promise<CheckoutPayment & { refund: CheckoutRefund | null }>;

Pay a sandbox link, as a buyer would. Test keys only. The link’s amount picks the outcome; listCheckoutSandboxScenarios() is the table.

simulateCheckoutPayment(
linkId: string,
p?: { reference?: string; clientReferenceId?: string; idempotencyKey?: string },
): Promise<{ id: string; status: CheckoutPaymentStatus }>;

Force an outcome now instead of waiting out the timeline. Test keys only.

simulateCheckoutPaymentOutcome(
paymentId: string,
action: 'refund' | 'chargeback' | 'fail',
idempotencyKey?: string,
): Promise<{ id: string; status: CheckoutPaymentStatus; moved: boolean }>;

What each amount does in the sandbox. Test keys only.

listCheckoutSandboxScenarios(): Promise<
{ suffix: string; description: string }[]
>;
createProduct(args: CreateProductArgs): Promise<CheckoutProduct>;
listProducts(): Promise<CheckoutProduct[]>;

Register a sandbox webhook endpoint. The secret is returned once and is not retrievable afterwards. Test keys only. Sends no Idempotency-Key and is never retried: a stored replay would keep the secret.

createWebhookEndpoint(args: { url: string; events?: string[] }): Promise<{
id: string;
url: string;
events: string[];
secret: string;
warning: string;
}>;

The webhook endpoints registered for your organization. Read-only by design, and there is no create/pause/delete counterpart here: a key that could repoint its own webhook URL could redirect every payout notification. The signing secret is never returned; it is shown once, at creation.

webhookEndpoints(): Promise<
{
id: string;
url: string;
events: string[];
/** Non-null while the endpoint is paused or auto-disabled. */
disabledAt: string | null;
disabledReason: 'paused_by_owner' | 'auto_disabled_after_failures' | null;
/** Retry ladders exhausted since the last accepted delivery. */
consecutiveFailures: number;
lastSuccessAt: string | null;
lastFailureAt: string | null;
createdAt: string;
}[]
>;

The 50 most recent delivery attempts for one endpoint, newest first. No payloads. Replay from the dashboard if you need the body, which re-fires under the same eventId so your dedupe still holds.

webhookEndpointDeliveries(endpointId: string): Promise<
{
id: string;
/** The event id: the `svix-id` we sent and the feed row's `id`. Dedupe on this. */
eventId: string;
eventType: string;
attempts: number;
/** Null until an attempt is accepted. */
deliveredAt: string | null;
/** When we will try again. Retries back off over roughly 70 hours. */
nextAttemptAt: string | null;
/** Your server's response on the most recent failure. */
lastError: string | null;
createdAt: string;
}[]
>;

What we sent, what came back, and what we retried. Sandbox endpoints.

webhookDeliveries(endpointId: string): Promise<unknown>;

Was this page helpful?