Skip to main content
Pay out to any @moovehandle or wallet address, across any supported chain.
Base URL https://api.moove.xyz. Generated against openapi.json.

moove.xyz never signs

moove.xyz is non-custodial. It holds no user signing keys, and no endpoint on this API accepts one.So there is no “send money” call. POST /v1/quote/send returns an unsigned transaction. You sign and broadcast it yourself, with a key that stays in your own environment. POST /v1/transaction then records a hash that is already on chain.This means a leaked Moove API key still cannot move a single cent. The only key that can is yours, and moove.xyz never receives, stores or transmits it.

The four steps

1

Quote the payout

POST /v1/quote/send returns a quote whose steps[].transactionData is unsigned. With a key, this is also where a funding wallet you have not connected by signature is refused — before you have signed anything.
2

Sign and broadcast — locally

With your own key, using your own library. moove.xyz is absent from this step entirely. Go through steps in order, skip any whose chainType is not your wallet’s, and keep the last hash.
3

Record it

POST /v1/transaction with the broadcast hash and the quote you signed. Returns a Moove id. Safe to retry with the same body if the response is lost.
4

Poll to settlement

GET /v1/transaction/{id} until status is settled.

Before you start

You need four things. Three take a minute; the fourth is the one people miss.
1

An API key scoped to the Moove Send Agent

Create it at moove.xyz/dashboard/api-keys and pick Moove Send Agent in the permissions picker. That grants send:quote, send:create and transaction:read — all three, because a payout needs to quote, record and poll.The plaintext key is shown once. Copy it straight into your secret manager.Check the key is set before your first call. See the warning below.Moove API Keys →
2

The base URL

Production is https://api.moove.xyz. It is shown with the key when you create it, and it is not a secret — but do not guess it, because a key is only valid against the deployment it was minted in.
3

A funding wallet connected BY SIGNATURE

This is the one that catches people. See the warning below — read it before you write any code.Its address cannot be discovered through the API — no endpoint a key can call lists your wallets. Read it once from Settings → Account → Wallet on the web, or from Wallet in your profile in the Moove App, and hold it in your own config.
4

A local signer

moove.xyz ships none, and this API cannot sign. You need a wallet library in your own project — viem, ethers, @solana/web3.js, whatever you already use — holding a key moove.xyz never sees.

Finding a chain id and a token address

POST /v1/quote/send takes numeric chain ids and token contract addresses, not names. Two endpoints give you both. Neither is listed in openapi.json — they are large catalogues rather than integration endpoints — but both are served, and both work without a credential:
GET /v1/chain lists every supported chain with its numeric id. GET /v1/token lists tokens with their contract addresses and decimals — scoped to one chain with chain_id (not chainId), or searched across every chain by symbol or name with q. EVM chains use their standard chain ids (1 for Ethereum, 8453 for Base, and so on). The three non-EVM chains do not — this API gives them its own numeric ids, and guessing a name like "solana" will not work:
Treat GET /v1/chain as authoritative, not this table. Bitcoin and Tron are rolled out separately and are not returned to every caller — they are Moove App only today. Read the list back rather than hardcoding anything beyond the chain you launch on.
Supported Blockchains → · Supported Tokens →

Before your first payout

fromAddress must be a wallet you connected to your Moove account by signing with it — in the Moove App or the dashboard.A receive-only address, one you added by pasting it into settings, cannot be the source of a send. The quote in step 1 refuses it with 409 SEND_ACCOUNT_NOT_READY — before you have signed anything, so nothing has moved.This is the most common integration failure, and it is not retryable: no amount of retrying will connect a wallet. Pasting an address is not proof that you hold its key, and a send has to be attributable to whoever actually signed it.The record call in step 3 runs the same check again. If the wallet is disconnected between your quote and your record, the 409 arrives there instead — after the funds have moved. Reconnect the wallet and record the same hash again. Do not re-broadcast.
Moove Wallet →
A missing key is not an error on three of these four routes. POST /v1/quote/send, POST /v1/transaction and GET /v1/transaction/{id} also serve callers with no credential — the Moove Widget and hosted checkout price and record payments logged out. So a request without an X-API-Key header is not refused: it runs anonymously.An anonymous call gets none of the key’s checks — no required addresses, no connected-wallet 409, no refusal of a destination moove.xyz operates — nothing is attributed to your key, and the record and poll responses come back redacted, without your description. Only GET /v1/transaction answers a missing key with 401 UNAUTHENTICATED.An unset or empty key variable is the usual cause: curl drops a header whose value is empty, so -H "X-API-Key: $MOOVE_API_KEY" then sends no header at all. Have your code stop before the first call if the key is empty.A key that is sent but is unknown, revoked or deactivated always fails with 401 INVALID_API_KEY (or 401 EXPIRED_API_KEY). It never falls back to anonymous.

Quote a send

endpoint
required
Requires the send:quote scope.
Prices a payout and returns the transaction data to execute it.
The steps[].transactionData in the response is unsigned. You sign and broadcast it yourself. This endpoint moves nothing.
A quote is not a reservation. Prices, fees and routes move. Quote immediately before you sign, and re-quote rather than signing a stale one.
With an API key, fromAddress and toAddress are both required, and fromAddress must be a wallet connected to your Moove account by signature. Each rule is checked here, before you sign anything, so a refusal costs you nothing:Both fields are optional only for anonymous pricing, which is how the Moove Widget shows an indicative price before a wallet is connected. That price is never meant to be signed.
Each address must be of its own leg’s chain family — with or without a key. The families are EVM, SVM (Solana), TVM (Tron) and BVM (Bitcoin), read from the chain catalogue.These are the same codes a missing address gets. The message tells them apart by naming both families, for example toAddress is on the EVM chain family, but chain 792703809 is SVM. A cross-family send is fine as long as each address matches its own chain: an EVM fromAddress on chain 1 paying a Solana toAddress on chain 792703809 is quoted normally.

Body

string
required
Amount to send, denominated in the source token. Must be greater than 0.Send it as a string to avoid floating-point rounding — "10.5", never 10.5. A JSON float cannot represent every token amount exactly, and here the rounding error is a real mispaid invoice.Bounded to the amount column’s precision: at most 78 digits with at most 18 decimal places. "0", a negative value, "NaN" and "Infinity" are all rejected with a 422.
string
required
The wallet funding the send. Required with an API key.It must be a wallet connected to your Moove account by signature. A receive-only address you added by pasting it cannot send, and the quote refuses it with 409 SEND_ACCOUNT_NOT_READY before you sign.Omitting it returns 400 INVALID_SOURCE. Without that refusal, the quote would be built against a placeholder source rather than your wallet, and the transaction you signed would not be yours to record. A value that is not a valid address on any supported chain returns 422 INVALID_DESTINATION — the address validator shares the destination code.A valid address of the wrong chain family for fromChainId — an EVM address with a Solana chain id — returns 400 INVALID_SOURCE, with or without a key.
string
required
The recipient. Required with an API key. Accepts two forms:A handle is lowercase letters, digits and underscores, 3 to 63 characters. "@Alice" and "@ab" both fail. There is no alias resolution on this API — a dotted name like "alice.moove.xyz" is not a handle. Anything that is neither a valid handle nor a valid address is rejected with 422 INVALID_DESTINATION. Resolve any alias of your own to a handle or an address before calling.With a key, omitting it — or naming a wallet moove.xyz operates itself, which no payout may be sent to — returns 400 INVALID_DESTINATION.An address of the wrong chain family for toChainId returns 400 INVALID_DESTINATION, with or without a key. A handle is checked through the wallet it pays: its owner’s default wallet. So "@alice" with a Solana toChainId, when Alice’s default wallet is on an EVM chain, returns 400 INVALID_DESTINATION — quote a toChainId of that wallet’s family.A well-formed handle that belongs to nobody fails the quote with 500 CANNOT_CREATE_QUOTE. Check the handle rather than retrying.
string
required
Chain id the funds leave from, e.g. "1" for Ethereum mainnet.
string
required
Chain id the funds arrive on. May differ from fromChainId — a cross-chain send is quoted and routed for you, including one to a different chain family, as long as toAddress is of this chain’s family.
string
required
Contract address of the token being sent, on fromChainId.
string
required
Contract address of the token the recipient receives, on toChainId. May differ from fromTokenAddress.

Request

Response

TransactionStepData[]
required
The transactions to execute, in order. A cross-chain or cross-token payout can return more than one — often an approval, then the transfer.Sign and broadcast them in order, waiting for each to confirm, and skip any step whose chainType is not your wallet’s. Record the last step’s hash: settlement is tracked by the hash you record, so recording an approval leaves the transaction processing for good. It does not error. How to sign each chainType →
string
required
The amount leaving the source wallet.
string
required
The amount the recipient receives, after routing and fees.
TokenData
required
The source token, including its chain.
TokenData
required
The destination token, including its chain.
number
required
Expected seconds to settlement. Use it to size your first poll delay.
string
required
Estimated network fee on the source chain, in sourceBlockchainToken.
string | null
The protocol cost for this route, in protocolCostToken.
TokenData | null
The token the protocol cost is charged in.
TokenData
required
The token the source-chain network fee is paid in.
string
required
The funding wallet, as you sent it.
string
required
The recipient’s wallet address. A handle you sent is resolved to its owner’s wallet here.
number
required
toAmount per unit of fromAmount.
string
required
USD value of fromAmount. toAmountUsd, sourceBlockchainFeeAmountUsd and protocolCostAmountUsd are the same for their amounts. Indicative only — 0 when a token has no price.
string | null
The route provider that priced the quote.
serviceFeeAmount, serviceFeeAmountUsd and serviceFeeToken are deprecated and always null — the fee is protocolCost*. fiatAmount, fiatCurrency and quoteToken belong to fiat payment links and are null on a send quote.
Keep the whole quote object. POST /v1/transaction needs the exact one you built the signed transaction from — not a re-quote, and not a subset of its fields.

Record a broadcast transaction

endpoint
required
Requires the send:create scope.
The same endpoint records a swap or bridge with "product": "swap" and the swap:create scope — see Moove Swap. Records a transaction you have already signed and broadcast yourself, and starts tracking it to settlement.
This endpoint sends nothing and signs nothing. By the time you call it, the funds have already moved. Its job is to attribute the on-chain transaction to your Moove account and begin polling it.
A retry of the same body is safe. With a key, re-posting the same body — the same transaction hash and the same product — for a send your account already recorded returns that same transaction with a 200, not an error. So if the response is lost — a timeout, a dropped connection, a 5xx — retry the identical request and read the id from whatever comes back.Anything else that reuses a recorded hash returns 500 CANNOT_PROCESS_EXISTING_TRANSACTION: another account’s hash, the same hash with a different product, or a re-post made without a key. Retrying will not change it.
quote.fromAddress is checked again here. The quote has already refused an unconnected wallet, so a 409 SEND_ACCOUNT_NOT_READY at this step means the wallet was disconnected in between — and the funds have already moved. Reconnect the wallet and record the same hash again. Do not re-broadcast; that would send twice.

Body

string
required
The on-chain hash you just broadcast — the last step’s, when the quote had more than one.
QuoteData
required
The quote object from POST /v1/quote/send — the same one you built the signed transaction from.
string
required
"send".Required when authenticating with a key, because the scope the request needs is derived from it. A missing or unsupported value is rejected with 422 INVALID_TRANSACTION_PRODUCT rather than defaulted — defaulting it would pick a permission on your behalf.
string
Your own reference — payout id, invoice number. Maximum 500 characters.Set it. It is how a human reconciles the row in the Moove dashboard back to what it paid for, and it is returned to you on the polling read.

Request

Response

string
required
The Moove transaction id. Poll this.
string
required
processing, settled or failed. A freshly recorded send is processing.
string
required
The on-chain hash you supplied.
string
required
The funding wallet.
string
required
The recipient’s resolved wallet address.
string
required
Amount sent, in the source token.
string
required
Amount received, in the destination token.
string | null
Your reference, as supplied.
string
required
send, receive or swap, read from your side of the row. A payout you recorded is send.
string
required
When the transaction was recorded, ISO 8601 in UTC.
TokenData
required
The token sent, including its chain. destinationToken is the token received.
string | null
The hash on the destination chain, for a cross-chain send once it is known.
string | null
The source hash on the chain’s block explorer. url is the transaction’s moove.xyz page.
Recipient identity fields are null for a key. destinationUserId, destinationUserHandle and destinationProfileImage come back empty — and for a call made without a key, both sides’ identity does.You supplied the destination, so you already know who you paid. Returning the recipient’s Moove profile would turn this endpoint into an address-to-identity lookup for anybody willing to send dust.

Poll a transaction

endpoint
Accepts a key carrying transaction:read, send:create or swap:create, a session, or no credential at all. A key holding none of those — a Receive-only key — gets 403.
Returns a single transaction by id, refreshing its on-chain status.
This endpoint is served but is not listed in openapi.json. Its absence from the schema is not a removal — it returns full user objects on both sides, a larger payload than an integration needs, so it is not advertised on the documented surface. It is the endpoint to poll.

Request

cURL
Poll politely. Start after executionDuration seconds from the quote, then check every few seconds with backoff. A tight loop will get you a 429 and will not make the transaction settle sooner.
Calling this with your key is what returns your own description. An anonymous read of the same id — the public /tx/<id> share link — has identity fields and the memo redacted, because the reader is not a party to the transaction.Recipient identity stays redacted for key holders either way, exactly as on the record call above.

List transactions

endpoint
required
Requires the transaction:read scope.
Returns the transactions belonging to the authenticated user, newest first, 10 per page.
A key sees every transaction its owner can see — including sends made from the Moove App and by a sibling key. This is the endpoint to reconcile a batch against, rather than polling ids one by one.

Query parameters

string
Filter by status. One of processing, settled, failed. Omit for all.
integer
default:"0"
Page offset. Follow nextOffset from the previous response.

Request

cURL

Response

A page of the same objects POST /v1/transaction returns, with limit, offset and nextOffset. Pagination →

Rate limits

Concrete numbers, so you pace a payout loop rather than meeting the cap as a 429: Each limit applies per API key and per source IP, whichever is reached first. Ten payouts a minute is 600 an hour. There is no batch endpoint — a batch is your own loop over these calls, so sleep between iterations. Rate Limits →

Errors

Branch on code, never on message.
A 409 from the record call is not a lost payment. The funds moved on chain regardless — POST /v1/transaction only records. Reconnect the wallet, then record the same hash again. Do not re-broadcast; that would send twice.
Errors →

Not in the API

moove.xyz does not sign, and ships no signer. There is no SDK, no key storage and no example that writes a key to disk. Signing is done by your own code, with your own library.Webhooks cover payment links only — there is no webhook for a payout yet. Poll GET /v1/transaction/{id}, or reconcile with GET /v1/transaction.

Next

Moove Swap

Swapping and bridging your own funds.

Authentication

Keys, scopes and the header.

Moove Agentic Payments

Build the integration with a prompt.

Moove Send

The product, in the Moove App.