# Quickstart (/start/quickstart)



<!-- generated from docs/wallet-api/QUICKSTART.md §4 — edit THAT file -->

## 4.1 Create a customer [#41-create-a-customer]

```bash
curl -X POST https://api.molnpay.com/v1/customers \
  -H "Authorization: Bearer $WALLET_KEY" -H "Content-Type: application/json" \
  -d '{"reference_id":"user_4471","email":"u@example.com"}'
```

```json
{ "object":"customer", "id":"9ab3…", "reference_id":"user_4471",
  "email":"u@example.com", "status":"active", "metadata":{},
  "created_at":"2026-08-01T10:00:00.000Z" }
```

`reference_id` is your own user id. Re-posting the same one returns the **existing**
customer with `200` instead of creating a duplicate.

## 4.2 Get a deposit address [#42-get-a-deposit-address]

```bash
curl -X POST https://api.molnpay.com/v1/wallets \
  -H "Authorization: Bearer $WALLET_KEY" -H "Content-Type: application/json" \
  -d '{"customer_id":"9ab3…","chain":"tron"}'
```

```json
{ "object":"wallet", "id":"7c1e…", "customer_id":"9ab3…", "chain":"tron",
  "role":"customer", "status":"active", "address":"TR7NHq…", "memo":null,
  "created_at":"2026-08-01T10:00:05.000Z" }
```

There is &#x2A;*one wallet per (customer, chain)**, enforced by the database. Calling
this again returns the same address with `200`. That is deliberate: deriving a
second address would split the customer's deposits across addresses your UI does
not show.

**TON requires the `memo`.** If `memo` is non-null, the sender must include it or
the funds are unattributable.

**Which chains, and what kind of address.** Read `GET /v1/chains` before you
render a picker — it is already filtered to what the platform lists for you.
Each chain carries:

* `account_kind` — `contract` is a keyless smart account: &#x2A;*the same address on
  every EVM chain marked `contract`**, no private key anywhere. `eoa` is a
  keypair the platform's signer holds. Keypair chains (Bitcoin, Dogecoin,
  Solana, TON, …) are listed only when the operator has switched them on;
  today the platform issues smart accounts only.
* `status` (`live` | `paused` | `coming_soon`) and `status_note`, the
  operator's reason in their words. Show the note verbatim.
* `deposits_enabled` at the chain level, and per asset under `assets[]`. A
  new address is issued only when both are true.

A request for a **new** address on a chain that is not issuing is refused with
`422` and a code that names the level: `account_kind_disabled` (the kind of
address that chain hands out is switched off), `chain_not_live_yet` or
`chain_deposits_disabled`. A quote for a coin whose own deposits are off
(invoices) fails with `asset_deposits_disabled`. **An address the customer
already holds is always returned**, whatever the switches say, and funds sent
to it keep being credited — the one exception is a coin the operator has
*archived* (a compromised contract), whose transfers are not credited.

## 4.3 The customer sends funds → you get webhooks [#43-the-customer-sends-funds--you-get-webhooks]

Register where they go first (§9.6 has the whole lifecycle):

```bash
curl -X POST https://api.molnpay.com/v1/webhooks \
  -H "Authorization: Bearer $WALLET_KEY" -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/molnpay/webhook","event_types":["deposit.confirmed","deposit.orphaned"]}'
```

The `201` carries the endpoint's `secret` (`whsec_…`) **once**. Store it; §9.3
is how you use it. Then:

```
deposit.detected   seen on chain. NOT credited. Show as "pending".
deposit.confirmed  passed the chain's confirmation threshold. Credited. This is money.
deposit.orphaned   a confirmed deposit was reorged out. Reverse what you credited.
```

## 4.4 Read the balance [#44-read-the-balance]

```bash
curl "https://api.molnpay.com/v1/customers/9ab3…/balances" \
  -H "Authorization: Bearer $WALLET_KEY"
```

`available` is spendable. `locked` is reserved against an in-flight payout.

## 4.5 Send a payout [#45-send-a-payout]

```bash
curl -X POST https://api.molnpay.com/v1/payouts \
  -H "Authorization: Bearer $WALLET_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: 3f7a1c92-payout-8812" \
  -d '{"customer_id":"9ab3…","chain":"tron","asset":"USDT","decimals":6,
       "amount":"12500000","to_address":"TQn9Y…","reference":"wd_8812"}'
```

Returns **202** — accepted, not yet sent. The transaction hash arrives later via
`payout.submitted` / `payout.confirmed`.

Above your configured threshold a payout enters `pending_approval` and waits for
a human. That is a safety feature, not an error.

### Withdrawing from the customer's own address (brokerages, exchanges) [#withdrawing-from-the-customers-own-address-brokerages-exchanges]

Send `"source": "customer_account"` (or have MolnPay make it your account's
default) and a payout leaves **only** from the paying customer's own deposit
address on that chain — the address they deposited to. `from_wallet_id` names
one of their wallets and implies it.

When that address holds less than the payout — a client who made money — we
first move the shortfall into it from where your coins sit (another of your
customers' addresses, or our custody), then send from the customer's address.
Meanwhile the payout stays `queued` with `waiting_on: "account_topup"`. The
customer's **balance** must still cover the payout: credit it first (a trade
settled on your books), or the request is refused with `insufficient_funds`.

EVM chains and Tron only. Elsewhere: `422 strict_source_unsupported` — send it
with `source: "auto"`.

## 4.6 Take a payment: invoice and hosted checkout [#46-take-a-payment-invoice-and-hosted-checkout]

```bash
curl -X POST https://api.molnpay.com/v1/invoices \
  -H "Authorization: Bearer $CHECKOUT_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: order_8812" \
  -d '{"price_amount":"4990","price_currency":"USD","reference_id":"order_8812",
       "title":"Order #8812","return_url":"https://shop.example/thanks"}'
```

```json
{ "object":"invoice", "id":"9f3c1b2e-…", "status":"open",
  "price_amount":"4990", "price_currency":"USD",
  "received_amount":"0", "excess_amount":"0",
  "checkout_url":"https://pay.molnpay.com/i/ck_9f3c…",
  "expires_at":"2026-09-16T12:30:00.000Z",
  "addresses":[{"chain":"bsc","address":"0x71C7…","memo":null}],
  "quotes":[{"chain":"bsc","asset":"USDT","contract":"0x55d3…",
             "amount_atomic":"49900000000000000000","amount":"49.90",
             "decimals":18,"rate":"1.000000000000000000","rate_source":"bitget_ws"}] }
```

Send the payer to `checkout_url`. That is all the integration there is: the page
shows the address, the QR and the amount, watches the chain, and returns them to
your `return_url`.

**`price_amount` is minor units.** `4990`, not `49.90`. `amount_atomic` on each
quote is what the payer actually sends; `amount` beside it is derived for
display and is never what a payment is checked against.

**`decimals` is per (chain, asset), and it is not decorative.** USDT is an
18-decimal token on BSC and a 6-decimal token everywhere else. The quoted amount
is computed with the number in this field, so use that field rather than a
constant of your own.

**Treat `checkout_url` like a bearer credential.** The token in it is the entire
authorisation for the checkout page — it is deliberately not the same value as
`id`, so that pasting an invoice id into a support ticket hands over nothing.

### What the payer sees, and what you get [#what-the-payer-sees-and-what-you-get]

|                         |                                                                                                                                                                                                                                                                 |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Paid within ±0.5%       | `invoice.paid`. Fulfil here, and only here.                                                                                                                                                                                                                     |
| Short by more than that | `invoice.underpaid`. The address stays live, the page shows the remainder with its own QR, and the window is extended (up to three times) so nobody loses it mid-transfer. &#x2A;*Do not fulfil.** A second `invoice.underpaid` is a new fact, not a duplicate. |
| Over by more than that  | `invoice.overpaid`, with `excess_amount` saying by how much. Your overpayment policy decides what happens to it: `keep` credits it to you, `refund` offers it back on the page.                                                                                 |
| Nothing, in time        | `invoice.expired`. Money arriving afterwards is still credited and marked `late`.                                                                                                                                                                               |

`invoice.pending` fires when a transfer is seen on chain but is below finality.
It is **not money** — a reorg can take it away — so use it to update a screen,
never to release goods.

### Cancelling [#cancelling]

```bash
curl -X POST https://api.molnpay.com/v1/invoices/9f3c1b2e-…/cancel \
  -H "Authorization: Bearer $CHECKOUT_KEY"
```

Only while nothing has been received. Cancelling does **not** close the payment
address: it stays watched forever, so a late transfer is still detected and
credited rather than lost.

### Refunding [#refunding]

```bash
curl -X POST https://api.molnpay.com/v1/invoices/9f3c1b2e-…/refund \
  -H "Authorization: Bearer $PAYOUT_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: refund_order_8812" \
  -d '{"to_address":"0xPAYER…","reason":"order cancelled"}'
```

Returns some or all of what a `paid`, `overpaid` or `late` invoice brought in,
in the coin it was paid in, to an address you name. Omit `amount` for
everything still refundable; give `amount` (atomic, as a string) **and**
`decimals` for a partial one, and repeat up to the bound. The invoice's
`paid_in` block says what that bound is right now.

**A `payout` key, signing on, `Idempotency-Key` required.** A refund is money
out to an address of your choosing, so it carries the payout gates — your
plan's payout fee from the prepaid balance, the approval threshold, the daily
line (past it the refund waits for our review; it is never refused for it) —
except the allowlist, because the destination is the payer's. `201` is a
promise: the refund is `sent&#x60; while the payout is queued or in flight.
**`invoice.refunded` fires when it confirms on chain**, and
`invoice.refund_failed` if it does not, with the amount back on your balance.
Once the whole receipt has gone back the invoice reads `refunded`.

Read them back under `GET /v1/refunds` (any key kind), including the
overpayment excess the payer directed from the checkout page.

## 4.7 Invoice knobs: tolerance, overpayment policy, charging the payer [#47-invoice-knobs-tolerance-overpayment-policy-charging-the-payer]

Four optional fields on `POST /v1/invoices` tune what a payer owes and how
tight the payment band is, per invoice, at creation (they are frozen with the
price and never recomputed):

| Field                | Default        | What it does                                                                                                                                        |
| -------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `tolerance_bps`      | 50             | The under/overpayment band in basis points of the payer amount (0–500). A payment within the band of the payer amount is `paid`, not under or over. |
| `overpayment_policy` | your account's | `keep` (record the excess) or `refund` (open a payer-directed refund) for THIS invoice.                                                             |
| `fee_to_payer_bps`   | 0              | Charge your processing fee to the payer: this many bps (0–2000) are added to what the payer sends — Cryptomus's `subtract`.                         |
| `markup_bps`         | 0              | A surcharge on the payer, bps of the price (0–2000), applied before `fee_to_payer_bps`.                                                             |

The invoice echoes the composed figure as &#x2A;*`payer_amount`*&#x2A; — what the
checkout asks the payer to send, priced into every quote — and
&#x2A;*`payer_fee_cents`**, the slice on top of your `price_amount`. `price_amount`
stays what YOU net. The settlement band centres on `payer_amount`, so a payer
who sends exactly what the page says is `paid`, never `overpaid` because a
fee sat on top. Omit every knob and the invoice behaves exactly as it did
before they existed.

```bash
curl -X POST https://api.molnpay.com/v1/invoices \
  -H "Authorization: Bearer $CHECKOUT_KEY" -H "Content-Type: application/json" \
  -d '{"price_amount":"5000","title":"Pro plan","tolerance_bps":100,
       "overpayment_policy":"refund","fee_to_payer_bps":150}'
# → price_amount "5000", payer_amount "5075" (1.5% to the payer), ±$0.50 band
```

Per-invoice `webhook_url` is deliberately not offered — endpoints carry
secrets, replay protection and an audit trail, and every event goes to the
endpoints you manage under §9.
