{"_id":"@avvio/payments","_rev":"5-4b497dc90a107fb002d6ef1d57f10434","name":"@avvio/payments","dist-tags":{"latest":"0.8.1"},"versions":{"0.1.0":{"name":"@avvio/payments","version":"0.1.0","keywords":["payouts","cross-border","mcp"],"license":"MIT","_id":"@avvio/payments@0.1.0","maintainers":[{"name":"avvio","email":"jaswanth@avvio.xyz"}],"homepage":"https://avvio-docs.pages.dev","bugs":{"url":"https://github.com/anzolabs/anzolabs-B2B-backend/issues","email":"support@avvio.xyz"},"bin":{"avvio-payments":"src/cli.js"},"dist":{"shasum":"b43c93839223cba918b0dd88db3658b7d27c180d","tarball":"https://registry.npmjs.org/@avvio/payments/-/payments-0.1.0.tgz","fileCount":11,"integrity":"sha512-+utXOU4rP5gY0OT7ckUP1qQeLK1wkx7rhtpyMXjv+Pw76xOOpAR7CQIcXqcSWXFezUfTM244vPFSuvzqrUqeQg==","signatures":[{"sig":"MEYCIQCm/UVKa6MhJhEDkVDQBaCWRwfdrf1r0Vy5tQ3XA6AU3AIhAJ0zjlErBsPU2NgE19rRNobyTKusn5pGyyCxVq9Lu9rz","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":144716},"main":"src/client.js","types":"index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./index.d.ts","default":"./src/client.js"},"./webhooks":{"types":"./index.d.ts","default":"./src/webhooks.js"},"./package.json":"./package.json"},"gitHead":"7abab2117b469936bbb7b22c9f4b24bfa8928a7c","scripts":{"test":"node --test test/*.test.js"},"_npmUser":{"name":"avvio","email":"jaswanth@avvio.xyz"},"repository":{"url":"git+https://github.com/anzolabs/anzolabs-B2B-backend.git","type":"git","directory":"packages/avvio-payments"},"_npmVersion":"11.12.1","description":"Pay out to your own customers from your Avvio balance. CLI, MCP server, and Node client — zero dependencies.","directories":{},"_nodeVersion":"26.0.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/payments_0.1.0_1787038702289_0.4183449063243625","host":"s3://npm-registry-packages-npm-production"}},"0.5.0":{"name":"@avvio/payments","version":"0.5.0","keywords":["avvio","cross-border","cross-border-payments","mass-payments","mcp","payout-api","payouts","stablecoin","usdc"],"license":"MIT","_id":"@avvio/payments@0.5.0","maintainers":[{"name":"avvio","email":"jaswanth@avvio.xyz"}],"homepage":"https://docs.avvio.xyz","bugs":{"url":"https://github.com/anzolabs/anzolabs-B2B-backend/issues","email":"support@avvio.xyz"},"bin":{"avvio-payments":"src/cli.js"},"dist":{"shasum":"362f75ab3d8a012dd8a65d8bd1b9645cf53ed97c","tarball":"https://registry.npmjs.org/@avvio/payments/-/payments-0.5.0.tgz","fileCount":11,"integrity":"sha512-kL/xgSvQkbQIl73g70dTqj/AOYC0Ec0RBffkZGX/dN7QDM3XCwhj/AufuRDnr1CQLwvPPCfkUKADRIEgZZD7eg==","signatures":[{"sig":"MEUCIQDzPsTJgTocuPyi2sSjDTc4JIg53uMKevevHO1KW9Y40AIgTkM3VVzjXkRwM0xOLwyO8aa30E+/xC2yWv0tjNQvSX4=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEQCIBZS2rPAFsJWtcybnJNm426SPCg6z3T35vzVrYI8Kr7XAiBeleWHhiLK7OMthy6DGCzJQIhxdA+vgAPQoRCP1VgEbg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":247532},"main":"src/client.js","types":"index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./index.d.ts","default":"./src/client.js"},"./webhooks":{"types":"./index.d.ts","default":"./src/webhooks.js"},"./package.json":"./package.json"},"gitHead":"1f8657e37b0d2c95a04196d3793f12d253189288","scripts":{"test":"node --test test/*.test.js"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:0a144223-a735-41ce-9d5e-b2d85186757a"}},"repository":{"url":"git+https://github.com/anzolabs/anzolabs-B2B-backend.git","type":"git","directory":"packages/avvio-payments"},"_npmVersion":"11.19.1","description":"Pay out to your own customers from your Avvio balance. CLI, MCP server, and Node client — zero dependencies.","directories":{},"_nodeVersion":"22.23.2","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/payments_0.5.0_1789171792110_0.18736081544605332","host":"s3://npm-registry-packages-npm-production"}},"0.7.0":{"name":"@avvio/payments","version":"0.7.0","keywords":["avvio","checkout","cross-border","cross-border-payments","mass-payments","mcp","payment-links","payout-api","payouts","stablecoin","usdc"],"license":"MIT","_id":"@avvio/payments@0.7.0","maintainers":[{"name":"avvio","email":"jaswanth@avvio.xyz"}],"homepage":"https://docs.avvio.xyz","bugs":{"url":"https://github.com/anzolabs/anzolabs-B2B-backend/issues","email":"support@avvio.xyz"},"bin":{"avvio-payments":"src/cli.js"},"dist":{"shasum":"40ea54388c89ee144bc8fef1d1d70a2efe369750","tarball":"https://registry.npmjs.org/@avvio/payments/-/payments-0.7.0.tgz","fileCount":11,"integrity":"sha512-VmJa4Qj2FhupCpbVVFcTW9EscZokNlRHNPL1mlrRtgCi0ynuaTPw/GP7JxQWJQrSrQQYIcg48mO0fAx2djABng==","signatures":[{"sig":"MEYCIQDFJvvxfKFE0u8Txv5ta+Cqpv9f55H6/gTMu1dGmTqY7QIhAK40/Keb3zP8E2JokxG6uzZIrLtociH+J3qp4AdpyDjP","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEQCIDjv+H7EkSLJmG1+Cg1NAMOe7XsGT4DFOV2O51nrlmCfAiBd90vk9FdjvND6r6d6I15sB/UOd/C7Gjwsjorwe5tmMQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":298199},"main":"src/client.js","types":"index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./index.d.ts","default":"./src/client.js"},"./webhooks":{"types":"./index.d.ts","default":"./src/webhooks.js"},"./package.json":"./package.json"},"gitHead":"64e5c6ab2702ffc5094b3f3b3aca94eed54bf7e4","scripts":{"test":"node --test test/*.test.js"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:0a144223-a735-41ce-9d5e-b2d85186757a"}},"repository":{"url":"git+https://github.com/anzolabs/anzolabs-B2B-backend.git","type":"git","directory":"packages/avvio-payments"},"_npmVersion":"11.20.0","description":"Pay out to your own customers from your Avvio balance, and accept payments on your website. CLI, MCP server, and Node client — zero dependencies.","directories":{},"_nodeVersion":"22.23.2","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/payments_0.7.0_1790440211425_0.3668545688614546","host":"s3://npm-registry-packages-npm-production"}},"0.8.0":{"name":"@avvio/payments","version":"0.8.0","keywords":["avvio","checkout","cross-border","cross-border-payments","mass-payments","mcp","payment-links","payout-api","payouts","stablecoin","usdc"],"license":"MIT","_id":"@avvio/payments@0.8.0","maintainers":[{"name":"avvio","email":"jaswanth@avvio.xyz"}],"homepage":"https://docs.avvio.xyz","bugs":{"url":"https://github.com/anzolabs/anzolabs-B2B-backend/issues","email":"support@avvio.xyz"},"bin":{"avvio-payments":"src/cli.js"},"dist":{"shasum":"98b2348fa259083fa2e76f460e7f7295eca96c15","tarball":"https://registry.npmjs.org/@avvio/payments/-/payments-0.8.0.tgz","fileCount":11,"integrity":"sha512-bxQQB4BWHDrZ/XQ3SCtJv4g1bYrlGM/sWSukcKCHlEfZZpZouKcTHD8i5x6MzfxajgkdRQCDI+EpnR45Tz8NhA==","signatures":[{"sig":"MEUCIC6t0mkRZWWX4JdiXJ3LY9b8c7bdJE6URdC5DMr85Pz8AiEAlp40jaifd0GtryMmqltMZDdunloRLiW/ZqSFYvu0i0g=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEQCIC1b6/LL/CaGvmj91FJvdUV7zz1DAOWmDdpQPFpj/acNAiAbIXpD4uX5AbVVHr1BOn/nuQ5mLncqCGTsWki82BiBOg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":308424},"main":"src/client.js","types":"index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./index.d.ts","default":"./src/client.js"},"./webhooks":{"types":"./index.d.ts","default":"./src/webhooks.js"},"./package.json":"./package.json"},"gitHead":"7900d5dc25bb346c81faa2d24498af3a5cdc87b0","scripts":{"test":"node --test test/*.test.js"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:0a144223-a735-41ce-9d5e-b2d85186757a"}},"repository":{"url":"git+https://github.com/anzolabs/anzolabs-B2B-backend.git","type":"git","directory":"packages/avvio-payments"},"_npmVersion":"11.20.0","description":"Pay out to your own customers from your Avvio balance, and accept payments on your website. CLI, MCP server, and Node client — zero dependencies.","directories":{},"_nodeVersion":"22.23.2","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/payments_0.8.0_1790476308424_0.9627186684217879","host":"s3://npm-registry-packages-npm-production"}},"0.8.1":{"_id":"@avvio/payments@0.8.1","bin":{"avvio-payments":"src/cli.js"},"bugs":{"url":"https://github.com/anzolabs/anzolabs-B2B-backend/issues","email":"support@avvio.xyz"},"dist":{"shasum":"79f0ee8eaf66b16bb6e962b78bb3ded356e626cd","tarball":"https://registry.npmjs.org/@avvio/payments/-/payments-0.8.1.tgz","fileCount":11,"integrity":"sha512-X3HQR4YUhwXil+KD9UPwL8jTprqKa0Bh/6HJMexFa8JCFEJqGnRFHCv1ZPQrP419mHC74yL9+XnUiGaPC3/eMg==","signatures":[{"sig":"MEQCIB6UMFnWdUBUSOuTe7PaxZ4Vpw6zhcmqqUxb5uPVxWbSAiBuDQEiDSGEdK2C6va7+C2PWBzmwMmNa80mdCFAF0otuw==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQD5ehjT3vpqkyGekxiDWqAI663lvdAf3NjMDmTsFcjQdgIgBb5OVZpMAczoXO/+gpRIzvyZkRqp9S7RhfaRuqhZFug="}],"unpackedSize":308964},"main":"src/client.js","name":"@avvio/payments","types":"index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./index.d.ts","default":"./src/client.js"},"./webhooks":{"types":"./index.d.ts","default":"./src/webhooks.js"},"./package.json":"./package.json"},"gitHead":"7ca6713c3e71045c6aa5dc104f9bb66e408b6ab0","license":"MIT","scripts":{"test":"node --test test/*.test.js"},"version":"0.8.1","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:0a144223-a735-41ce-9d5e-b2d85186757a"}},"homepage":"https://docs.avvio.xyz","keywords":["avvio","checkout","cross-border","cross-border-payments","mass-payments","mcp","payment-links","payout-api","payouts","stablecoin","usdc"],"repository":{"url":"git+https://github.com/anzolabs/anzolabs-B2B-backend.git","type":"git","directory":"packages/avvio-payments"},"_npmVersion":"11.20.0","description":"Pay out to your own customers from your Avvio balance, and accept payments on your website. CLI, MCP server, and Node client — zero dependencies.","directories":{},"maintainers":[{"name":"avvio","email":"jaswanth@avvio.xyz"}],"_nodeVersion":"22.23.2","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/payments_0.8.1_1790486539685_0.7922348995963662"}}},"time":{"created":"2026-08-18T07:38:22.122Z","modified":"2026-09-27T05:22:19.972Z","0.1.0":"2026-08-18T07:38:22.428Z","0.5.0":"2026-09-12T00:09:52.236Z","0.7.0":"2026-09-26T16:30:11.507Z","0.8.0":"2026-09-27T02:31:48.512Z","0.8.1":"2026-09-27T05:22:19.771Z"},"bugs":{"url":"https://github.com/anzolabs/anzolabs-B2B-backend/issues","email":"support@avvio.xyz"},"license":"MIT","homepage":"https://docs.avvio.xyz","keywords":["avvio","checkout","cross-border","cross-border-payments","mass-payments","mcp","payment-links","payout-api","payouts","stablecoin","usdc"],"repository":{"url":"git+https://github.com/anzolabs/anzolabs-B2B-backend.git","type":"git","directory":"packages/avvio-payments"},"description":"Pay out to your own customers from your Avvio balance, and accept payments on your website. CLI, MCP server, and Node client — zero dependencies.","maintainers":[{"name":"avvio","email":"jaswanth@avvio.xyz"}],"readme":"# @avvio/payments\n\nPay out to your own customers, from your balance, over one API.\n\n**Docs:** [docs.avvio.xyz](https://docs.avvio.xyz) · **Product:** [avvio.xyz](https://avvio.xyz) · **Partner program:** [avvio.xyz/partners](https://avvio.xyz/partners)\n\nOne package, three ways to use it: a **Node client**, a **CLI**, and an **MCP\nserver** so an agent can drive payouts directly. All three share the same core,\nso they cannot drift apart.\n\n## Zero dependencies\n\nNot a boast — a deliberate answer to a question your security review will ask.\n\nThis package sits inside your infrastructure holding a credential that moves\nmoney. Transitive npm compromise is the most realistic path to that credential,\nand the shortest answer to \"what is in this dependency tree\" is *nothing*. It\nalso means no peer conflicts inside your app and nothing to compile.\n\nNode 18 or newer.\n\n## Setup\n\n```bash\nnpm i @avvio/payments\n```\n\n```bash\nexport AVVIO_API_KEY=avvio_test_…   # complete bearer key; shown once\nexport AVVIO_ORG_ID=cmsx…   # a cuid, not an org_ prefix\nexport AVVIO_BASE_URL=https://api.avvio.xyz/business/api/v1   # optional\n```\n\nThe API key is the complete credential. Calling the API directly? Send it in\n`x-api-key`:\n\n```bash\ncurl \"$AVVIO_BASE_URL/recipients/$AVVIO_ORG_ID/corridors\" \\\n  -H \"x-api-key: $AVVIO_API_KEY\"\n```\n\nOnly operations marked idempotent in the API reference take an\n`Idempotency-Key`; generate it once and reuse it with every retry of the same\nlogical request.\n\n> **Treat the complete API key like a database password.** Keep it in\n> server-side secret storage. Use only an `avvio_test_*` key in ReadMe's Try It\n> console and never embed a key in your own browser or mobile application.\n\nStart here:\n\n```bash\nnpx -y @avvio/payments doctor\n```\n\nIt checks the key, the organization, connectivity and your balance, and names\nwhich one is wrong. \"It doesn't work\" is otherwise four indistinguishable\nproblems.\n\n## Sixty seconds to a payout\n\n```bash\nnpx -y @avvio/payments guide                     # the whole flow, as commands\nnpx -y @avvio/payments fund --amount 5000        # sandbox balance\nnpx -y @avvio/payments corridors                 # what you can pay\nnpx -y @avvio/payments requirements MXN          # what Mexico needs\nnpx -y @avvio/payments quote --amount 200 --to MXN\n\nnpx -y @avvio/payments beneficiary create \\\n  --name \"Maria Gonzalez\" --email maria@example.com \\\n  --currency MXN --end-user employee_42 \\\n  --field clabeNumber=012180000080004471\n\nnpx -y @avvio/payments pay --amount 200 --to <destinationAccountId> \\\n  --end-user employee_42 --expect 3410.00\n```\n\nUse an `avvio_test_` key and none of this touches a payment network. The **last four\ndigits** of the account number choose what happens — `0003` completes and is\nthen returned by the bank, which is the case worth testing before you go live.\n\n## Node\n\n```js\nconst { PayoutsClient } = require('@avvio/payments');\n// or, from ESM — the package declares an `exports` map, so named imports work:\n// import { PayoutsClient, stableKey } from '@avvio/payments';\n// import { verifyWebhook } from '@avvio/payments/webhooks';\n\nconst avvio = new PayoutsClient();\n\n// Which world this key pays into, from its prefix. Worth asserting in your own\n// test suite before anything sends: a live key in a payroll fixture pays real\n// people. The client rejects anything that is not a complete `avvio_*` credential.\nif (avvio.mode !== 'test') throw new Error('refusing to run tests against live');\n\n// Show the price while your user is still typing. No beneficiary needed.\nconst quote = await avvio.quote({ amount: '200.00', to: 'MXN' });\n// → { sourceAmount, destinationAmount, fee, rate, limits, indicative: true }\n// quote() calls GET /rates: an indicative price. The two-step flow's binding\n// quote is POST /quotes/offramp (`pricePayout()` here), which needs a beneficiary.\n\nconst beneficiary = await avvio.createBeneficiary({\n  name: 'Maria Gonzalez',\n  email: 'maria@example.com',\n  currency: 'MXN',\n  endUserId: 'employee_42',          // scopes it to ONE of your users\n  externalId: 'emp42_maria',         // makes a repeat create safe\n  details: { clabeNumber: '012180000080004471' },\n});\n\nconst payout = await avvio.payout({\n  amount: '200.00',\n  destinationAccountId: beneficiary.method.destinationAccountId,   // the account this call registered\n  expectDestination: quote.destinationAmount.amount,   // read this next\n  endUser: { id: 'employee_42', name: 'Ana Lopez' },\n  reference: 'ZZ-2026-0042',\n});\n```\n\n**`expectDestination` is the one option not to skip.** (The CLI spells it `--expect`; the wire field is\n`expectDestination`. This client also accepts `expectDestinationAmount` and\ntranslates it — but that is a courtesy of the Node client only, so if you are\ngenerating a client from the OpenAPI spec, `expectDestination` is the only name\nthat exists.) Between the quote\nyou showed your user and the send, the rate can move. Pass the number you\npromised and the send is refused if it has drifted more than 2% — nothing is\nsent, and you re-quote. Without it, you ship whatever the market did in between.\n\n## Retrying safely\n\n**Persist your own `idempotencyKey` before you send, and reuse it on every\nretry.** That single habit is what prevents a double payment; everything else on\nthis page is a safety net under it.\n\nWe generate a key when you omit one, which makes a single call safe — and means\n**calling `payout()` again is not a retry**, it is a second payment with a second\nkey. A job that crashes and requeues without having persisted its key will\neventually pay a wage twice. Our server-side duplicate check covers 15 minutes of\nthat gap; a backoff longer than 15 minutes is outside it, by design and by\narithmetic.\n\nRetry with the *same* key and you get the original result, never a second\npayment.\n\n**If you would rather not persist one, derive it.** `stableKey()` hashes the\nthings that make the payment unique into a UUID, so the same payroll row\nproduces the same key on every attempt — including the attempt after the crash,\nwhere there is nothing left in memory to reuse and nothing was written down:\n\n```js\nconst { stableKey } = require('@avvio/payments');\n\nawait avvio.payout({\n  ...args,\n  idempotencyKey: stableKey(orgId, payrollRunId, employeeId),\n});\n```\n\nPass whatever identifies *this* payment and nothing that changes between\nattempts — a timestamp or a retry counter in there defeats the whole thing.\nParts are NUL-separated, so `('a','bc')` and `('ab','c')` are different keys,\nand an empty part throws rather than quietly collapsing two people's wages onto\none key.\n\n**A timeout is an unknown outcome, not a failure.** If a send times out the\npayout may exist. The key is on the error:\n\n```js\ntry {\n  await avvio.payout({ ...args });\n} catch (err) {\n  if (err.type === 'TIMEOUT') {\n    // Same key. A replay returns the original payout; a new one pays twice.\n    await avvio.payout({ ...args, idempotencyKey: err.idempotencyKey });\n  }\n}\n```\n\nThe drift guard throws the same `RATE_DRIFT_EXCEEDED` you would get over\nHTTP, so one branch handles both surfaces.\n\n`err.retryable` tells you whether retrying unchanged is worth it. A `409`\nconflict is **not** retryable — it means the same key was used with a different\nbody, which is a bug on your side.\n\n### The retry that quietly defeats all of this\n\n`payout()` generates a key when you do not pass one. That makes the safe thing\nthe default for a single call — and it means **calling `payout()` again is not a\nretry**, it is a second payment with a second key. An HTTP client that mints a\nkey per attempt has the same shape, and it is the most common way a retry turns\ninto a double charge: key-based replay never fires, because every attempt looks\nlike a new request.\n\nSo we watch a second signal server-side: same body, different key, inside 15\nminutes. You get a `409` instead of a payment. It applies only to routes that\nmove money — registering the same beneficiary twice is not a duplicate payment\nand is not refused.\n\n```js\ntry {\n  await avvio.payout({ ...args });\n} catch (err) {\n  if (err.type === 'DUPLICATE_REQUEST_DETECTED') {\n    // Nothing was sent. Two ways forward — pick one, we will not guess.\n    await avvio.payout({ ...args, idempotencyKey: err.originalIdempotencyKey });\n    // …or, if you really do mean to send it twice:\n    // await avvio.payout({ ...args, allowDuplicate: true });\n  }\n}\n```\n\nIt refuses rather than returning the first payout, because both readings happen.\nTwo advances of the same amount to the same worker in one week is ordinary\npayroll — replaying there would mean the second never goes out while your ledger\nsays it did. **The best outcome for a genuinely ambiguous request is a loud one.**\n\nRetention is seven days, and it is about storage, not correctness: there is no\n\"the key expired, so we ran it again\" path. A key we still hold replays; past\nseven days the record is gone and the key is unknown to us.\n\n## Webhooks\n\nFirst, get a signing secret. In sandbox you can issue one yourself:\n\n```bash\nnpx -y @avvio/payments webhook create --url https://example.ngrok-free.app/hooks\n# Signing secret (shown once — store it now):\n#   whsec_…\n```\n\nWhen calling the hosted API, use a publicly reachable HTTPS receiver; a\ndevelopment tunnel is fine. `http://localhost` works only when the Avvio backend\nitself is running locally, because a hosted backend resolves localhost to itself,\nnot to your laptop. Live endpoints are created from the dashboard by a human —\na credential that could repoint its own webhook URL could quietly redirect every\npayout notification, so that one stays off the API.\n\nThe secret is shown **once** and is not retrievable. Store it before you close\nthe terminal.\n\n```bash\nnpx -y @avvio/payments webhook deliveries <endpointId>   # a sandbox endpoint you registered\nnpx -y @avvio/payments webhook endpoints                 # what is registered on your org\nnpx -y @avvio/payments webhook attempts <endpointId>     # the last 50 attempts, newest first\n```\n\n`attempts` answers \"did you send me that event\" without opening a dashboard:\neach row carries `eventId` — the `svix-id` we sent, so it is what your dedupe\nkeys on — plus `attempts`, `lastError` (your server's own response) and\n`nextAttemptAt`. Retries back off over roughly 70 hours; after that only the\nevent feed still has it. Payloads are not returned, and a dashboard replay\nre-fires under the same `eventId`, so your deduplication still holds.\n\n```js\nconst { verifyWebhook } = require('@avvio/payments');\n\napp.post('/hooks/avvio', express.raw({ type: '*/*' }), (req, res) => {\n  let event;\n  try {\n    event = verifyWebhook({\n      body: req.body,          // the RAW bytes, not a parsed object\n      headers: req.headers,\n      secret: process.env.AVVIO_WEBHOOK_SECRET,\n    });\n  } catch {\n    return res.sendStatus(400);\n  }\n  res.sendStatus(200);         // ack fast, process after\n  handle(event);\n});\n```\n\nVerify over the **raw** body. Re-serializing a parsed object does not reproduce\nthe same bytes, and one reordered key fails every signature.\n\nWorth doing once before you go live: verify a real event with the **wrong**\nsecret and confirm your handler rejects it. A verification step that silently\npasses is indistinguishable from no verification at all until someone posts you\na forged payout.\n\nTwo things to build for:\n\n- **Webhooks are the fast path, not the guarantee.** `getPayout()` is\n  authoritative. Treat a webhook as the nudge to look.\n- **`completed` is not always final.** A bank can return a settled payment days\n  later, giving `failed` with `returned_by_bank` and `fundsReturned: true`. Do\n  not write a ledger that treats `completed` as immutable.\n\n## Reconciling\n\nThe event feed is the durable half of the pair above: every transition, in\norder, each with a `sequence` you carry forward. A webhook you missed is gone;\nan event is still there.\n\n```bash\navvio-payments events --since 4102          # a page, plus the nextSince to keep\navvio-payments events --payout-id pay_01J…  # everything that happened to one\navvio-payments events --follow              # tail it, one JSON row per line\n```\n\n`--follow` polls and prints new rows as they land, which is what a terminal is\nfor. **It holds the watermark in memory only** — it is a tail, not a\nreconciler. When it stops, the gap is read again only if you persisted a\n`nextSince` and pass it back as `--since`. In Node, `eachEvent()` pages for you:\n\n```js\nfor await (const event of avvio.eachEvent({ since: savedWatermark })) {\n  await apply(event);               // dedupe on event.id — the feed is at-least-once\n  savedWatermark = event.sequence;\n}\n```\n\nTo watch one payout instead of the whole feed:\n\n```bash\navvio-payments status pay_01J… --watch\n```\n\nIt polls until the payout stops moving and prints the final payout to stdout,\nwith progress on stderr so `--json` still pipes. It stops on `completed`\nbecause nothing further happens *on this payout* — a bank return arrives days\nlater as a new event, which is why it tells you to keep reading the feed.\n\n## Payouts you fund yourself\n\nSome routings hand you the payout with `requiresFunding: true` instead of\ndebiting a balance. The money stays in your wallet until you move it; we hold no\nkey and cannot move it.\n\n```bash\navvio-payments funding pay_01J…                       # address, amount, network, expiry\navvio-payments funding confirm pay_01J… --tx 0xabc…   # after you have broadcast it\n```\n\n`funding` is a pure read — poll it as often as you like. `expiresAt` comes back\n`null`: there is no countdown on the deposit address, so ask `status <payoutId>`\nwhether an old set of instructions is still good rather than running a timer.\n\n`confirm` reports a transfer that has **already left your wallet**, so it is\nstrict about the hash: a truncated paste is refused here, before anything is\nreported, rather than coming back as a rejection that reads like your transfer\nfailed. Pass `--idempotency-key` if you want a timed-out confirm to be safely\nrepeatable. Bare `funding`, with no payout id, is still the other question —\nwhere to wire a top-up for your balance.\n\n## Mass payouts\n\nUp to 1,000 payouts in one request. Each line of `items` is exactly a\n`POST /payouts` wire body, and the **idempotency key covers the run** — a\nsubmit loop that dies and resubmits the same file under the same key gets the\nsame batch back, never a second payroll. `externalReferenceId` is **required\nand unique per organization**: the same run id under a fresh key is refused\nwith `PAYOUT_BATCH_DUPLICATE_REFERENCE` naming the original batch, which is\nwhat stops a crashed submit job from paying a payroll twice. A corrected\nresubmission is a new run and needs its own id. Batches are on by default\n(`MASS_PAYOUTS_DISABLED` only if your organization opted out) and submission is\nlimited to 30 requests a minute.\n\n```js\nconst batch = await avvio.createPayoutBatch({\n  externalReferenceId: 'payroll-2026-09-01',   // REQUIRED: your run id, unique per org\n  idempotencyKey: 'payroll-2026-09-01-run1',   // persist this BEFORE you send\n  items: [\n    { amount: '200.00', destinationAccountId: 'acct_…', reference: 'PR-0042' },\n    { amount: '150.00', destinationAccountId: 'acct_…', reference: 'PR-0043' },\n  ],\n});\n```\n\nThe `202` means **received, not paid**: every line is validated first (nothing\npriced, nothing debited). With `autoCommit: true` — the default — a clean run\ngoes straight to creation; any validation errors and it holds at\n`awaiting_confirmation` for your call:\n\n```js\nlet run = await avvio.getPayoutBatch(batch.batchId);\nif (run.status === 'awaiting_confirmation') {\n  const bad = await avvio.listPayoutBatchItems(batch.batchId, { status: 'invalid' });\n  // each item echoes your instruction back verbatim — join on content\n  await avvio.confirmPayoutBatch(batch.batchId);   // proceed with the valid lines\n  // …or avvio.cancelPayoutBatch(batch.batchId) to stop the whole run\n}\n```\n\nA batch tracks **creation, not settlement**: `completed` means every line\neither became a payout or was refused — read `run.counts`. Join the run to your\nledger with `listPayoutBatchItems(batchId, { status: 'created' })`; each\ncreated line carries a `payoutId` that lives the ordinary payout lifecycle —\n`payout.*` webhooks, the event feed, `getPayout()`.\n\nOne item status is special: **`requires_review` means the outcome is unknown**\n(the process died mid-create). It is never retried automatically — re-running a\nline that may already have paid is how a crash becomes a double payment —\nand it is never folded into `create_failed`. Contact support with the\n`batchId`. `listPayoutBatches({ externalReferenceId })` finds your runs, and\nthe `payout_batch.*` webhooks (`awaiting_confirmation`, `completed`,\n`canceled`, `failed`) arrive on the same signing and retry ladder as\n`payout.*`.\n\n## Accepting payments on your website\n\nMoney arriving, on the same key. Create a **checkout link**, send the buyer to\nits `shareUrl`, fulfil on the `checkout_payment.paid` webhook. A **test key**\nworks: it addresses your sandbox organization, which is already verified and\ncard-ready, and `simulateCheckoutPayment(linkId)` pays a link. The link's\namount picks the outcome — a total ending `.04` is paid and then charged back,\nwhich is the case worth rehearsing most.\n\n```js\nconst product = await avvio.createProduct({\n  name: 'Consulting (60 min)', currency: 'USD', unitAmount: '150.00',\n});\n\nconst link = await avvio.createCheckoutLink({\n  productId: product.id,\n  successUrl: 'https://example.com/thanks',   // card payments redirect here\n  cancelUrl: 'https://example.com/pricing',\n  clientReferenceId: order.id,                // your join key, echoed on every event\n  metadata: { orderId: order.id },\n  publish: true,                              // live now; shareUrl is in the response\n});\nif (link.status !== 'sent') {\n  // Publish was refused (card onboarding unfinished, say). The draft is kept\n  // and returned rather than thrown, so a retry cannot make a second one.\n  throw new Error(link.publishError.message);\n}\nres.redirect(link.shareUrl);\n```\n\nThen, in the webhook handler above, branch on the new family. Subscribe to it\nexplicitly when registering the endpoint: an endpoint with an empty `events`\nlist gets every payout type but **not** `checkout_payment.*`.\n\n```js\nif (event.type === 'checkout_payment.paid') {\n  const { clientReferenceId, amount, currency, paymentId } = event.data;\n  const order = await orders.find(clientReferenceId);\n  if (order && order.total === amount && order.currency === currency) {\n    await fulfil(order, paymentId);\n  }\n}\n```\n\nThe success page must not trust the redirect (it carries\n`?avvio_link=<slug>&client_reference_id=<ref>`); confirm with the webhook or\n`avvio.listCheckoutPayments(link.id)`, whose amounts are base units with\n`decimals` beside them. `paid` is not final: `checkout_payment.refunded`,\n`.partially_refunded` and `.reversed` can follow. Held bank deposits and open\ndisputes fire no event; read them off the payment. Refund with\n`refundCheckoutPayment(paymentId, { amount?, reason? })` on a key that holds\nthe `refunds` scope (an owner or admin grants it at issuance), or from the\ndashboard. `pauseCheckoutLink()` is permanent. The full guide is\n[Accept payments](https://docs.avvio.xyz/docs/checkout-setup).\n\n## MCP\n\nFor an agent that pays people, or builds the integration. Add both servers:\n`avvio-payments` acts on your organization with your key, and `avvio-docs`\nsearches the documentation (no key).\n\nClaude Code:\n\n```bash\nclaude mcp add avvio-payments -e AVVIO_API_KEY=avvio_test_… -e AVVIO_ORG_ID=… -- npx -y @avvio/payments mcp\nclaude mcp add --transport http avvio-docs https://avvio-docs.pages.dev/mcp\n```\n\nCursor, Claude Desktop and other MCP clients:\n\n```json\n{\n  \"mcpServers\": {\n    \"avvio-payments\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@avvio/payments\", \"mcp\"],\n      \"env\": { \"AVVIO_API_KEY\": \"avvio_test_…\", \"AVVIO_ORG_ID\": \"…\" }\n    },\n    \"avvio-docs\": { \"type\": \"http\", \"url\": \"https://avvio-docs.pages.dev/mcp\" }\n  }\n}\n```\n\nOn connect, `avvio-payments` tells the agent how to pay someone correctly:\nthe order of calls, idempotency and unknown outcomes, bank returns after\n`completed`, and the sandbox test accounts. It also offers two prompts:\n\n- **`integrate_payouts`**: plan and build the integration in your codebase\n  (bank form, recipients, idempotent payouts, webhooks, reconciliation, tests).\n  Takes `stack` and `use_case`.\n- **`sandbox_walkthrough`**: send one test payout and follow it until it is\n  paid and then returned by the bank.\n\nThirty-four tools: `list_corridors`, `get_requirements`, `quote`, `create_beneficiary`, `list_beneficiaries`, `get_beneficiary`, `get_beneficiary_by_external_id`, `update_beneficiary`, `delete_beneficiary_method`, `list_payment_reasons`, `send_payout`, `get_payout`, `list_payouts`, `funding_accounts`, `list_events`, `list_approvals`, `get_approval`, `list_audit_events`, `get_policy`, `get_balance`, `list_balance_transactions`, `get_funding`, `create_payout_link`, `create_product`, `list_products`, `create_checkout_link`, `update_checkout_link`, `get_checkout_link`, `list_checkout_links`, `pause_checkout_link`, `list_checkout_payments`, `refund_checkout_payment`, `confirm_funding`, `cancel_payout`.\nTwenty-two reads are marked read-only so a host can auto-approve them.\nApproving a held payout has no tool, by design: a key cannot approve, and\nneither can an agent holding one.\n\n> **Six tools move money or do something irreversible**, and each requires an\n> explicit `confirm: true`: `send_payout`, `refund_checkout_payment`,\n> `confirm_funding`, `cancel_payout`, `delete_beneficiary_method` and\n> `pause_checkout_link` (a paused link cannot be republished). `send_payout`\n> and `refund_checkout_payment` additionally require an `idempotencyKey`, so a\n> half-parsed instruction cannot become a payment and a reflexive retry cannot\n> become two. Start with a test key.\n\nSome client methods are deliberately NOT agent tools: `deleteBeneficiary`\nremoves a whole record and every method on it, `getBeneficiaryMethodDetails`\nreturns full account numbers that do not belong in a model's context when\n`last4` answers the question, and the webhook reads are ops work rather than\npayout work.\nThe batch methods (`createPayoutBatch` and its five companions) are excluded\ntoo: one confirm committing up to 1,000 payments does not belong behind a\nsingle tool call — an agent pays one person at a time via `send_payout`.\nReach for those from Node or the CLI.\n\n## CLI reference\n\n| Command | |\n|---|---|\n| `guide` | The whole flow, as commands you can paste |\n| `doctor` | Check credentials, connectivity, balance |\n| `fund [--amount]` | Credit your sandbox balance |\n| `balance` | What you can currently send |\n| `balance-transactions [--cursor] [--type]` | Every change to your balance, newest first, with `balanceAfter` |\n| `policy` | Your caps, approval threshold, features and rate limits. Read it first |\n| `corridors` | Currencies you can pay out to |\n| `requirements <CCY>` | Fields that corridor needs |\n| `quote --amount --to` | Price with no beneficiary |\n| `beneficiary create` | Register who is paid. `--external-id` makes a repeat create safe |\n| `beneficiary list [--end-user]` | Saved beneficiaries |\n| `beneficiary get <id>` | One beneficiary. `--external-id` looks it up by YOUR id |\n| `beneficiary update <id>` | Contact details only — bank details are not editable |\n| `beneficiary delete <id>` | Removes them and every payment method on them |\n| `beneficiary method delete <id> <methodId>` | Removes one account; the rest stay payable |\n| `beneficiary method details <id> <methodId>` | The full account on file, not just `last4` |\n| `payment-reasons` | Reasons a payout may state, where a corridor asks for one |\n| `pay --amount --to [--expect]` | Send. `--expect` refuses the send if the rate moved |\n| `status <payoutId> [--watch]` | One payout, live. `--watch` polls until it stops moving |\n| `payouts` | Recent payouts |\n| `events [--since] [--type] [--follow]` | The change feed. `--type` narrows it; `--follow` tails it as JSON lines |\n| `approvals [--status]` | Payouts and runs waiting on your approvers (a 202 from `pay` or a batch confirm) |\n| `approval <approvalId>` | One approval; `executed` ones name the `payoutId` |\n| `audit-events [--action] [--cursor]` | Who did what, with which credential, newest first |\n| `funding` | Where to wire a top-up |\n| `funding <payoutId>` | Deposit instructions for a payout you fund yourself |\n| `funding confirm <payoutId> --tx` | Report the transfer you already sent |\n| `webhook endpoints` | Endpoints registered for your org (read-only) |\n| `webhook attempts <endpointId>` | Last 50 delivery attempts: `eventId`, `attempts`, `lastError` |\n| `product create --name --currency --amount` | A checkout catalog product |\n| `product list` | Your catalog |\n| `checkout create --product [--success-url] [--ref] [--meta k=v]` | Create and publish a checkout link; prints `shareUrl`. `--draft` keeps it unpublished |\n| `checkout get <linkId>` | One link, with what it has received |\n| `checkout list [--status]` | Your links, newest first |\n| `checkout pause <linkId>` | Stop it taking payments. Permanent |\n| `checkout payments <linkId>` | Payments on one link, newest first |\n| `mcp` | Run as an MCP server |\n\nEvery command takes `--json`.\n\n## What can change under you\n\n`CHANGELOG.md` says exactly which parts of this API we may change without\nwarning and which we will not. The short version: **branch on `type` and on\n`status`, ignore fields you do not recognise, and never treat `completed` as\nfinal** — a bank can return a settled payment days later.\n\n## Errors\n\n`PayoutsError` carries `type` (stable — branch on this, not the message),\n`status`, `requestId`, `idempotencyKey`, and `retryable`.\n\nIn TypeScript, `type` is the `PayoutsErrorType` union of every documented code,\nso a `switch` over it is checked rather than a set of string literals nobody\nverifies. It keeps `(string & {})` in the union deliberately: new types ship\nwithout a major version, and an unrecognised one must still compile rather than\nbreak your build against a live API. Write the `default` branch.\n\nQuote the `requestId`\nwhen you contact us; it is in the body of every ERROR, and on every response as the `x-request-id` header.\n\n## License\n\nMIT. See [LICENSE](./LICENSE).\n","readmeFilename":"README.md"}