API reference

Payouts

Outbound transfers to external addresses: send funds to an external address; list payouts; what a payout would cost and what would arrive — without making it…

GET/v1/payouts
AuthorizationBearer <token>

Authorization: Bearer mp<64 hex> for live, mptest<64 hex> for test.

The token is the prefix plus 64 lowercase hex characters (32 CSPRNG bytes) and nothing else — no separators, no embedded fields — so nothing in it needs quoting, escaping or encoding. Send it in the Authorization header and nowhere else. Never put it in a query string: URLs reach access logs, proxy logs, browser history and Referer headers, and a key that lands in any of those is a leaked key. A key's KIND (checkout, payout or wallet) is a property of the key, not of the string: it is chosen at creation, returned by GET /v1/ping, and decides which routes the key may reach.

The full key is shown exactly once, at creation. No endpoint can return it afterwards. Moving funds additionally requires signing to be enabled globally, for your project, and for that key — so a read-only key is a supported configuration.

In: header

Query Parameters

limit?integer
Range1 <= value <= 100
Default25
cursor?string

Opaque cursor from the previous page's next_cursor.

customer_id?string
Formatuuid
chain?string

Value in

  • "ethereum"
  • "bsc"
  • "polygon"
  • "arbitrum"
  • "base"
  • "avalanche"
  • "tron"
  • "solana"
  • "ton"
  • "bitcoin"
  • "dogecoin"
  • "optimism"
  • "linea"
  • "zksync"
  • "scroll"
status?string
reference?string

Response Body

application/json

application/json

application/json

application/json

application/json

application/json

curl -X GET "https://example.com/v1/payouts"
{  "object": "list",  "has_more": true,  "next_cursor": "string",  "data": [    {      "object": "payout",      "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",      "ref": "MOLN-7F3A9C21B04E",      "customer_id": "160c0c4b-9966-4dc1-a916-8407eb10d74e",      "chain": "ethereum",      "asset": "USDT",      "asset_logo_url": "string",      "asset_id": "b4695157-0d1d-4da0-8f9e-5c53149389e4",      "amount": "1500000000000000000",      "amount_decimal": "string",      "fee_amount": "1500000000000000000",      "to_address": "string",      "memo": "string",      "status": "queued",      "reference": "string",      "tx_hash": "string",      "explorer_url": "string",      "network_fee": "21000000000000",      "network_fee_decimal": "string",      "network_fee_asset": "string",      "network_fee_usd": "string",      "amount_usd": "string",      "source": "auto",      "from_wallet_id": "8b9c84c1-d66f-45a0-b01d-9f87581202d6",      "release_at": "2019-08-24T14:15:22Z",      "waiting_on": "string",      "failure_code": "string",      "created_at": "2019-08-24T14:15:22Z",      "updated_at": "2019-08-24T14:15:22Z"    }  ]}
POST/v1/payouts

Returns 202 — accepted, not sent. The transaction hash arrives via the payout.submitted and payout.confirmed webhooks.

Idempotency-Key is REQUIRED: a retried POST without one can send funds twice.

decimals must match the asset's configured scale. This is checked because it is where the BSC trap bites — "1000000" is 1 USDT on Tron and 0.000000000001 USDT on BSC.

Above your auto-approve threshold the payout enters pending_approval and waits for a human. So does a payout that takes today's (UTC) total past your daily automatic line ($1,000 by default) — there is no daily cap, nothing is refused for the total, it is reviewed. That is a safety feature, not an error.

A destination on your allowlist is usable 24 hours after it was added (active_at on the destination); before then the payout is refused with destination_not_allowlisted, and the message says when it opens.

Cancel window. With a delay (your account's delay_seconds, see PUT /v1/payouts/settings, or this request's delay_seconds) the payout is accepted and its funds reserved at once, then held until release_at; until then POST /v1/payouts/{id}/cancel takes it back. A delay never defers a refusal: a customer whose balance cannot cover the payout gets 402 insufficient_funds now, and nothing is created. When the window closes the payout is canceled instead of sent if its destination left your allowlist or its API key was revoked meanwhile (payout.canceled with failure_code).

Console sessions (not API keys) must confirm every withdrawal with a second factor: passkey (an assertion over a challenge from POST /v1/merchant/passkeys/challenge) or totp_code. A session with neither factor enrolled is refused (403 second_factor_not_enrolled). API keys ignore both fields.

AuthorizationBearer <token>

Authorization: Bearer mp<64 hex> for live, mptest<64 hex> for test.

The token is the prefix plus 64 lowercase hex characters (32 CSPRNG bytes) and nothing else — no separators, no embedded fields — so nothing in it needs quoting, escaping or encoding. Send it in the Authorization header and nowhere else. Never put it in a query string: URLs reach access logs, proxy logs, browser history and Referer headers, and a key that lands in any of those is a leaked key. A key's KIND (checkout, payout or wallet) is a property of the key, not of the string: it is chosen at creation, returned by GET /v1/ping, and decides which routes the key may reach.

The full key is shown exactly once, at creation. No endpoint can return it afterwards. Moving funds additionally requires signing to be enabled globally, for your project, and for that key — so a read-only key is a supported configuration.

In: header

Header Parameters

Idempotency-Key*string

REQUIRED. A retry without one can send funds twice, so the request is rejected outright.

Lengthlength <= 255

Request Body

application/json

TypeScript Definitions

Use the request body type in TypeScript.

Response Body

application/json

application/json

application/json

application/json

application/json

application/json

application/json

application/json

application/json

application/json

curl -X POST "https://example.com/v1/payouts" \  -H "Idempotency-Key: string" \  -H "Content-Type: application/json" \  -d '{    "chain": "ethereum",    "asset": "USDT",    "decimals": 0,    "amount": "string",    "to_address": "string"  }'
{  "object": "payout",  "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",  "ref": "MOLN-7F3A9C21B04E",  "customer_id": "160c0c4b-9966-4dc1-a916-8407eb10d74e",  "chain": "ethereum",  "asset": "USDT",  "asset_logo_url": "string",  "asset_id": "b4695157-0d1d-4da0-8f9e-5c53149389e4",  "amount": "1500000000000000000",  "amount_decimal": "string",  "fee_amount": "1500000000000000000",  "to_address": "string",  "memo": "string",  "status": "queued",  "reference": "string",  "tx_hash": "string",  "explorer_url": "string",  "network_fee": "21000000000000",  "network_fee_decimal": "string",  "network_fee_asset": "string",  "network_fee_usd": "string",  "amount_usd": "string",  "source": "auto",  "from_wallet_id": "8b9c84c1-d66f-45a0-b01d-9f87581202d6",  "release_at": "2019-08-24T14:15:22Z",  "waiting_on": "string",  "failure_code": "string",  "created_at": "2019-08-24T14:15:22Z",  "updated_at": "2019-08-24T14:15:22Z"}
POST/v1/payouts/quote

The same body as POST /v1/payouts, minus the idempotency key. Nothing is created, reserved or locked, and no key that can send money is needed — payouts:read is enough.

The amount is not reduced by either fee. The plan's payout fee is a USD debit against your prepaid operational balance, and the network fee is MolnPay's — a withdrawal leaves the customer's keyless account through factory.execute(), which the chain operator calls and pays for. So receives equals amount, and total_debit_cents is what comes off the balance.

Hard refusals throw the same errors the create would — including no_master_wallet and custody_intake_pending — so you find out about a bad address or a chain with no sender here rather than at submit. The soft gates — needing approval (including past the daily automatic line, exceeds_daily_cap, which means review and never a refusal), an exhausted balance — are reported as FIELDS, because they are figures a person should see before deciding.

network_fee is an estimate and null where we cannot honestly make one. It is never zero: gas is never free, and a zero on that line reads as if it were.

AuthorizationBearer <token>

Authorization: Bearer mp<64 hex> for live, mptest<64 hex> for test.

The token is the prefix plus 64 lowercase hex characters (32 CSPRNG bytes) and nothing else — no separators, no embedded fields — so nothing in it needs quoting, escaping or encoding. Send it in the Authorization header and nowhere else. Never put it in a query string: URLs reach access logs, proxy logs, browser history and Referer headers, and a key that lands in any of those is a leaked key. A key's KIND (checkout, payout or wallet) is a property of the key, not of the string: it is chosen at creation, returned by GET /v1/ping, and decides which routes the key may reach.

The full key is shown exactly once, at creation. No endpoint can return it afterwards. Moving funds additionally requires signing to be enabled globally, for your project, and for that key — so a read-only key is a supported configuration.

In: header

Request Body

application/json

TypeScript Definitions

Use the request body type in TypeScript.

Response Body

application/json

application/json

application/json

application/json

application/json

application/json

application/json

application/json

application/json

curl -X POST "https://example.com/v1/payouts/quote" \  -H "Content-Type: application/json" \  -d '{    "customer_id": "160c0c4b-9966-4dc1-a916-8407eb10d74e",    "chain": "ethereum",    "asset": "USDT",    "decimals": 0,    "amount": "string",    "to_address": "string"  }'
{  "object": "payout_quote",  "chain": "string",  "asset": "string",  "decimals": 0,  "amount": "string",  "amount_decimal": "string",  "amount_usd_cents": "string",  "network_fee": {    "amount": "string",    "amount_decimal": "string",    "asset": "string",    "decimals": 0,    "usd_cents": "string",    "estimated": true  },  "network_fee_asset": "string",  "network_fee_paid_by": "molnpay",  "network_fee_held_cents": "string",  "network_fee_markup_cents": "string",  "network_fee_markup_bps": 0,  "service_fee_cents": "string",  "plan": "string",  "total_debit_cents": "string",  "receives": "string",  "receives_decimal": "string",  "balance_after_cents": "string",  "balance_short": true,  "requires_approval": true,  "auto_approve_usd": "string",  "daily_remaining_usd": "string",  "exceeds_daily_cap": true,  "available": "1500000000000000000",  "available_decimal": "string",  "funds_sufficient": true,  "delay_seconds": 0,  "to_address": "string",  "memo": "string"}
GET/v1/payouts/settings
AuthorizationBearer <token>

Authorization: Bearer mp<64 hex> for live, mptest<64 hex> for test.

The token is the prefix plus 64 lowercase hex characters (32 CSPRNG bytes) and nothing else — no separators, no embedded fields — so nothing in it needs quoting, escaping or encoding. Send it in the Authorization header and nowhere else. Never put it in a query string: URLs reach access logs, proxy logs, browser history and Referer headers, and a key that lands in any of those is a leaked key. A key's KIND (checkout, payout or wallet) is a property of the key, not of the string: it is chosen at creation, returned by GET /v1/ping, and decides which routes the key may reach.

The full key is shown exactly once, at creation. No endpoint can return it afterwards. Moving funds additionally requires signing to be enabled globally, for your project, and for that key — so a read-only key is a supported configuration.

In: header

Response Body

application/json

application/json

application/json

application/json

application/json

application/json

curl -X GET "https://example.com/v1/payouts/settings"
{  "object": "payout_settings",  "delay_seconds": 0,  "max_delay_seconds": 0}
PUT/v1/payouts/settings

A window in which your integration, or an owner or admin in the console, can cancel a payout that looks wrong (POST /v1/payouts/{id}/cancel). 0 = instant (the default); at most 86400 (24 hours).

The funds are reserved when the payout is accepted, so a held payout is always one that can be paid — an empty balance is refused with 402 insufficient_funds at once. Applies to payouts made with an API key; a withdrawal a person confirms in the console stays instant unless it asks for a delay. A change applies to the next payout; one already accepted keeps its release_at.

Needs a payout key with payouts:create, or an owner or admin in the console. Every change is audited.

AuthorizationBearer <token>

Authorization: Bearer mp<64 hex> for live, mptest<64 hex> for test.

The token is the prefix plus 64 lowercase hex characters (32 CSPRNG bytes) and nothing else — no separators, no embedded fields — so nothing in it needs quoting, escaping or encoding. Send it in the Authorization header and nowhere else. Never put it in a query string: URLs reach access logs, proxy logs, browser history and Referer headers, and a key that lands in any of those is a leaked key. A key's KIND (checkout, payout or wallet) is a property of the key, not of the string: it is chosen at creation, returned by GET /v1/ping, and decides which routes the key may reach.

The full key is shown exactly once, at creation. No endpoint can return it afterwards. Moving funds additionally requires signing to be enabled globally, for your project, and for that key — so a read-only key is a supported configuration.

In: header

Request Body

application/json

TypeScript Definitions

Use the request body type in TypeScript.

Response Body

application/json

application/json

application/json

application/json

application/json

application/json

application/json

curl -X PUT "https://example.com/v1/payouts/settings" \  -H "Content-Type: application/json" \  -d '{    "delay_seconds": 600  }'
{  "object": "payout_settings",  "delay_seconds": 0,  "max_delay_seconds": 0}
GET/v1/payouts/{id}
AuthorizationBearer <token>

Authorization: Bearer mp<64 hex> for live, mptest<64 hex> for test.

The token is the prefix plus 64 lowercase hex characters (32 CSPRNG bytes) and nothing else — no separators, no embedded fields — so nothing in it needs quoting, escaping or encoding. Send it in the Authorization header and nowhere else. Never put it in a query string: URLs reach access logs, proxy logs, browser history and Referer headers, and a key that lands in any of those is a leaked key. A key's KIND (checkout, payout or wallet) is a property of the key, not of the string: it is chosen at creation, returned by GET /v1/ping, and decides which routes the key may reach.

The full key is shown exactly once, at creation. No endpoint can return it afterwards. Moving funds additionally requires signing to be enabled globally, for your project, and for that key — so a read-only key is a supported configuration.

In: header

Path Parameters

id*string
Formatuuid

Response Body

application/json

application/json

application/json

application/json

application/json

application/json

application/json

curl -X GET "https://example.com/v1/payouts/497f6eca-6276-4993-bfeb-53cbbbba6f08"
{  "object": "payout",  "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",  "ref": "MOLN-7F3A9C21B04E",  "customer_id": "160c0c4b-9966-4dc1-a916-8407eb10d74e",  "chain": "ethereum",  "asset": "USDT",  "asset_logo_url": "string",  "asset_id": "b4695157-0d1d-4da0-8f9e-5c53149389e4",  "amount": "1500000000000000000",  "amount_decimal": "string",  "fee_amount": "1500000000000000000",  "to_address": "string",  "memo": "string",  "status": "queued",  "reference": "string",  "tx_hash": "string",  "explorer_url": "string",  "network_fee": "21000000000000",  "network_fee_decimal": "string",  "network_fee_asset": "string",  "network_fee_usd": "string",  "amount_usd": "string",  "source": "auto",  "from_wallet_id": "8b9c84c1-d66f-45a0-b01d-9f87581202d6",  "release_at": "2019-08-24T14:15:22Z",  "waiting_on": "string",  "failure_code": "string",  "created_at": "2019-08-24T14:15:22Z",  "updated_at": "2019-08-24T14:15:22Z"}
POST/v1/payouts/{id}/cancel

Releases the reserved funds back to the customer's available balance and returns the payout fee. Always succeeds while the payout is inside its cancel window (release_at in the future). Once signing has begun the chain owns the transaction and cancellation is impossible.

AuthorizationBearer <token>

Authorization: Bearer mp<64 hex> for live, mptest<64 hex> for test.

The token is the prefix plus 64 lowercase hex characters (32 CSPRNG bytes) and nothing else — no separators, no embedded fields — so nothing in it needs quoting, escaping or encoding. Send it in the Authorization header and nowhere else. Never put it in a query string: URLs reach access logs, proxy logs, browser history and Referer headers, and a key that lands in any of those is a leaked key. A key's KIND (checkout, payout or wallet) is a property of the key, not of the string: it is chosen at creation, returned by GET /v1/ping, and decides which routes the key may reach.

The full key is shown exactly once, at creation. No endpoint can return it afterwards. Moving funds additionally requires signing to be enabled globally, for your project, and for that key — so a read-only key is a supported configuration.

In: header

Path Parameters

id*string
Formatuuid

Response Body

application/json

application/json

application/json

application/json

application/json

application/json

application/json

application/json

curl -X POST "https://example.com/v1/payouts/497f6eca-6276-4993-bfeb-53cbbbba6f08/cancel"
{  "object": "payout",  "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",  "ref": "MOLN-7F3A9C21B04E",  "customer_id": "160c0c4b-9966-4dc1-a916-8407eb10d74e",  "chain": "ethereum",  "asset": "USDT",  "asset_logo_url": "string",  "asset_id": "b4695157-0d1d-4da0-8f9e-5c53149389e4",  "amount": "1500000000000000000",  "amount_decimal": "string",  "fee_amount": "1500000000000000000",  "to_address": "string",  "memo": "string",  "status": "queued",  "reference": "string",  "tx_hash": "string",  "explorer_url": "string",  "network_fee": "21000000000000",  "network_fee_decimal": "string",  "network_fee_asset": "string",  "network_fee_usd": "string",  "amount_usd": "string",  "source": "auto",  "from_wallet_id": "8b9c84c1-d66f-45a0-b01d-9f87581202d6",  "release_at": "2019-08-24T14:15:22Z",  "waiting_on": "string",  "failure_code": "string",  "created_at": "2019-08-24T14:15:22Z",  "updated_at": "2019-08-24T14:15:22Z"}