> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hypermid.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Deposit

> Let a payer fund a wallet you manage on their behalf.

Same shape as checkout, with the amount usually left **open** — the payer
decides how much to add.

```ts theme={"system"}
import { createDeposit } from "@hypermid/sdk";

const session = await createDeposit(
  {
    chain: 8453,
    token: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
    recipient: userWalletAddress,   // the wallet YOU manage for this user
    orderId: `deposit_${user.id}_${Date.now()}`,
    minAmount: "1000000",           // optional floor: 1 USDC
  },
  { secretKey: process.env.HYPERMID_SECRET_KEY! },
);
```

## Set up in the portal

For a server-created Deposit, the merchant needs an `sk_` key stored on a
server and a destination wallet for the person being funded. Add **Payer
choices** in the portal only when the payer should choose among configured
destination chain/token pairs; it does not replace the recipient on a simple
server-authored Deposit. Payout addresses are not required for this
merchant-managed recipient flow.

For a partner that Hypermid has enabled, a publishable key can create an open Deposit
directly in the browser with `@hypermid/sdk/publishable-deposit`, without a
merchant backend. The browser must use one destination-binding artifact; it
cannot name an arbitrary chain, token, or recipient. The production rollout is
partner-gated: configure the key's exact origin yourself, then ask Hypermid to
enable the partner; otherwise use this server-created flow, a registered
destination, or a payment link. Verify success
from a server-side session read or a signed webhook, not from the payer's
browser.

Omitting `amount` is what makes it open. `minAmount` / `maxAmount` bound it.

## Fees

A Deposit uses the same two-fee policy as Checkout and Withdrawal: the Hypermid fee
plus an optional developer fee on the same payer-input amount. The developer
fee defaults to 0 and is limited to 0–100 basis points. A partner Hypermid
override replaces the global rate outright.

Same-token EVM transfers are the zero fee exception because a plain transfer
cannot collect a fee. On EVM routes, developer earnings are held by Hypermid and
settled periodically by manual transfer. On the Near deposit-address rail, the
provider keeps half of each declared app fee and pays the developer leg directly
to the configured recipient.

## Let the payer choose what they receive

If your customer can receive funds on more than one chain, create the deposit
with `destinations` instead of one top-level destination. Each candidate binds
a chain, token, and recipient chosen by your server. The hosted payment page
shows the candidates in the same order you send them.

<Warning>
  `destinations` is mutually exclusive with the top-level `chain`, `token`,
  `recipient`, `amount`, `minAmount`, and `maxAmount` fields. Sending both shapes
  returns `400`.
</Warning>

Each candidate needs its **own recipient for that chain**. A Solana destination
needs a Solana address, a Sui destination needs a Sui address, and an EVM
destination needs an address on that EVM chain. Never reuse one address across
ecosystems merely because the same customer owns all of the wallets.

### The same destinations power browser-created Deposits

The destination candidates configured for a server-created Deposit are also the
choices a browser can select with a publishable key when Method 2 is enabled.
The browser submits the candidate's opaque `candidateId`; it does not submit a
replacement chain, token, or recipient. This keeps the destination bound to the
same merchant-controlled configuration instead of trusting a public key to name
where funds should go.

The other binding artifacts are a payer-signed SIWE challenge, which is
EVM-only, or a single-use grant minted by a merchant server. A grant therefore
does not provide a no-backend integration. See [Payments](/guides/payments) for
the three choices and the current 403 rollout errors, and the
[publishable-key SDK reference](/sdk/publishable-deposit) for exact types.

The `chain` inside each candidate is a **decimal string**, not a JavaScript
number. Keep it as a string end to end. Sui's chain ID,
`"9270000000000000"`, exceeds `Number.MAX_SAFE_INTEGER`; JavaScript does not
guarantee exact integer handling in that range, and other IDs can silently
round. Use `BigInt(chain)` only if you genuinely need arithmetic.

### Mixed Base, Solana, and Sui example

This example creates one open deposit that lets the payer choose USDC on Base,
native SOL on Solana, or native SUI on Sui. The three recipient variables must
be addresses from their respective ecosystems.

```ts theme={"system"}
const session = await createDeposit(
  {
    orderId: `deposit_${user.id}_${Date.now()}`,
    metadata: { userId: user.id },
    destinations: [
      {
        chain: "8453",
        token: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC
        recipient: baseRecipient,
        minAmount: "1000000", // 1 USDC, in this candidate's base units
      },
      {
        chain: "1151111081099710",
        token: "0x0000000000000000000000000000000000000000", // native SOL sentinel
        recipient: solanaRecipient,
      },
      {
        chain: "9270000000000000",
        token: "0x0000000000000000000000000000000000000000", // native SUI sentinel
        recipient: suiRecipient,
      },
    ],
  },
  { secretKey: process.env.HYPERMID_SECRET_KEY! },
);

// session.status === "awaiting_destination"
// the destination candidates preserve Base → Solana → Sui order
```

Only offer destinations enabled in your partner catalogue. Hypermid validates
every candidate when the intent is created; one disallowed or currently
unroutable candidate rejects the whole request instead of showing the payer an
option that cannot complete.

## Configure the destinations once, in the portal

`destinations` names the candidates on every request. If your accepted
`(chain, token)` pairs are the same for every payer, configure them **once** in
the partner portal and pass only a payer-owned recipient per chain with
`destinationRecipients` instead:

```ts theme={"system"}
const response = await fetch("https://server.hypermid.io/v1/payments/deposit", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.HYPERMID_SECRET_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    orderId: `deposit_${user.id}_${Date.now()}`,
    destinationRecipients: [
      { chain: "8453",             recipient: baseRecipient },   // EVM
      { chain: "20000000000001",   recipient: bitcoinRecipient }, // Bitcoin
      { chain: "9270000000000000", recipient: suiRecipient },     // Sui
    ],
  }),
});
```

The accepted token per chain comes from your portal configuration, so a
candidate here is just a chain plus the recipient that should receive it.

<Warning>
  `destinationRecipients` only works when your account is in **configured Deposit
  mode**. Without it the request is rejected with *"destinationRecipients requires
  configured Deposit mode"*. It is **Deposit only** — a checkout pays the
  merchant, so its recipient is not payer-owned.
</Warning>

The rules the backend enforces — each one is a real error to expect:

* A recipient is required for **every** configured chain. Omit one and the
  request fails.
* Each recipient is validated **against its chain**. An EVM address on the
  Bitcoin row is rejected.
* `destinationRecipients` is mutually exclusive with `chain` / `token` /
  `recipient` / `amount` / `minAmount` / `maxAmount` **and** with
  `destinations`. Sending either alongside it is a `400`.
* One configured pair produces an **ordinary session** directly — no intent, no
  picker, no selection call.

Multiple configured pairs behave like `destinations`: the response is an intent
with `status: "awaiting_destination"`, and the payer selects one before funding
(next section).

## Selecting and freezing a destination

Both multi-destination shapes (`destinations` and multi-pair
`destinationRecipients`) return an **intent**, not a session:
`status: "awaiting_destination"`. `GET /v1/payments/:id/public` returns its
`destinationCandidates`, each with an opaque `id`. Two endpoints move the payer
from an intent to a frozen session — mind which one you use:

| Verb   | Endpoint                       | Effect                                                                                                                                                 |
| ------ | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `PUT`  | `/v1/payments/:id/destination` | **Provisional.** Repeatable and reversible. Records a tentative choice; creates no session, quote, or deposit address. Returns the intent, still open. |
| `POST` | `/v1/payments/:id/destination` | **Freeze.** One-way. Copies the candidate's chain/token/amount/recipient atomically and materialises an ordinary session.                              |

Use `PUT` while the payer is still deciding — a picker that highlights the
current choice can call it as often as the payer changes their mind:

```ts theme={"system"}
await fetch(`https://server.hypermid.io/v1/payments/${intent.id}/destination`, {
  method: "PUT",                       // provisional — call again to change it
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ candidateId: selected.id }),
});
```

Then `POST` once, when the payer commits, to freeze the session:

```ts theme={"system"}
const selected = intent.destinationCandidates.find(
  (candidate) => candidate.chain === "1151111081099710",
);
if (!selected) throw new Error("Solana destination is not available");

const response = await fetch(
  `https://server.hypermid.io/v1/payments/${intent.id}/destination`,
  {
    method: "POST",                    // freeze — one-way
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ candidateId: selected.id }),
  },
);

if (!response.ok) throw new Error(`Destination selection failed: ${response.status}`);
const session = await response.json();
```

The `POST` freeze is irreversible. Repeating it with the **same** `candidateId`
is idempotent, but `POST`ing a **different** candidate after the first success
returns `409` — as does any `PUT` once the session is frozen. The freeze happens
before the flow issues a deposit address, because letting the destination change
after an address is shown could strand funds at an address the session no longer
tracks.

After the freeze, quote, payment, settlement, webhook, and session-read behavior
is the same as for any other deposit.

<Note>
  Existing integrations do not need to change. A request with top-level
  `chain` / `token` / `recipient` still creates the same single-destination
  session as before. A one-item `destinations` array also creates an ordinary
  session directly: no intent, no destination picker, and no selection call.
</Note>

## Funding from a non-EVM chain

If the payer holds BTC, SOL, TON or similar, there is no wallet that can sign an
EVM transaction — the session takes the [deposit flow](/concepts/execution-flows)
instead. Mint an address with `POST /v1/payments/{id}/deposit`, supplying
`originAsset` and `refundTo`.

<Warning>
  `refundTo` is required **up front**. If settlement fails there is no connected
  wallet to return funds to, so it cannot be collected afterwards.
</Warning>

Want a quote without reserving an address? `GET /v1/payments/{id}/deposit-quote`
prices it without minting.
