POST /payments
Creates a payment and returns its id plus the hosted-checkout URL to redirect the customer to. Requires the X-Api-Key header and the payment:create permission (granted to gateway API keys by default).
In the default hosted mode the response is deliberately small: your server only needs to redirect the customer to paymentUrl. The hosted checkout then loads the payable assets, deposit addresses, and amounts itself, and the authoritative outcome comes from verify — so per-asset offer details are not returned here. In API checkout mode (checkoutMode: "API") the response additionally inlines the full payment object so you can render your own payment page — see the API-mode response variant below.
curl -X POST https://api-staging.finomesh.com/api/v1/payments \
-H "X-Api-Key: $FINOMESH_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"currencyCode": "USD",
"amount": "49.99",
"callbackUrl": "https://shop.example.com/payment/result"
}'
Headers
| Header | Type | Required | Description | Example |
|---|---|---|---|---|
X-Api-Key | string | Yes | Gateway API key. Keep it server-side only. | <your-api-key> |
Content-Type | string | Yes | Must be application/json. | application/json |
Request body
| Field | Type | Required | Default | Description | Example |
|---|---|---|---|---|---|
currencyCode | string | Yes | — | Invoice currency as an ISO-style code (USD, EUR, case-insensitive). An unknown or inactive code is rejected with 422. | "USD" |
amount | string (decimal) | Yes | — | Invoice amount in currencyCode units. Decimal string — never a number, to preserve precision. | "49.99" |
type | string (enum) | No | DIRECT | DIRECT for a one-off payment, or PAYLINK for a payment created from a saved payment link. | "DIRECT" |
paylinkId | string (UUID) | No | null | The payment-link ID to bind this payment to. Provide when type is PAYLINK; omit for DIRECT. | "…" |
checkoutMode | string (enum) | No | HOSTED | HOSTED — Finomesh hosts the payment page — or API — you render it yourself and drive the flow server-to-server (see Embedded Checkout / API Mode). The mode flips the callbackUrl/webhookUrl contract, per the rows below. API additionally requires your server IPs to be whitelisted in the dashboard: 422 when no allowlist is configured at all, and 403 when the calling server's IP is not on it — so even a leaked API key cannot create API-mode payments from elsewhere. | "API" |
callbackUrl | string | Conditional | — | Absolute URL the customer's browser is redirected to after the result. Required in HOSTED mode; forbidden in API mode (there is no hosted page to redirect from — sending it returns 422). localhost is allowed in any environment. See Hosted Checkout. | "https://shop.example.com/payment/result" |
callbackParams | object | No | — | Arbitrary merchant JSON object echoed into the callback redirect's query string (e.g. your order ID). Hosted mode only. | { "orderId": "ORD-1042" } |
webhookUrl | string | Conditional | — | URL Finomesh POSTs the signed webhook to when the payment reaches a terminal status. Optional in HOSTED mode (no webhook is sent if omitted); required in API mode — the webhook is the only push channel for an embedded payment's outcome. | "https://shop.example.com/api/finomesh-webhook" |
webhookParams | object | No | — | Arbitrary merchant JSON object stored with the payment and echoed back for your own bookkeeping. | { "orderId": "ORD-1042" } |
timeoutSeconds | integer | No | 3600 | Payment validity window in seconds. Server default (3600 = 1 hour) applies if omitted or 0. After it the payment expires. | 3600 |
txFeePayer | string (enum) | No | gateway setting | Who absorbs the network transaction fee: MERCHANT or CUSTOMER. | "CUSTOMER" |
commissionFeePayer | string (enum) | No | gateway setting | Who absorbs the Finomesh commission: MERCHANT or CUSTOMER. | "CUSTOMER" |
overAcceptableDiff | string (decimal) | No | gateway setting | Overpayment tolerance. Decimal string — never a number. Interpreted per acceptableDiffKind. | "0.5" |
underAcceptableDiff | string (decimal) | No | gateway setting | Underpayment tolerance. Decimal string — never a number. | "0.5" |
acceptableDiffKind | string (enum) | No | gateway setting | How the two tolerances are read: ABSOLUTE (flat USD) or PERCENTAGE (percent of the expected USD amount). | "PERCENTAGE" |
checkAmountDeviation | boolean | No | gateway setting | Whether to apply the acceptance band at all. When false, any paid amount resolves to SUCCESS. See Amount Deviation. | true |
assets | array<OfferAsset> | No | auto-derived | Restrict/override the offered assets. Omit it (recommended) and the offers are derived automatically from your gateway's enabled-asset list at current prices. Element shape below. | — |
language | string | No | null | Preferred checkout display-language hint. | "en" |
isSandbox | boolean | No | false | Create a sandbox payment — no real deposit address, checkout shows simulation buttons. | false |
Platform-billed gateways. On a gateway with a platform-billed arrangement, Finomesh covers the network fee and the commission on every payment — nothing is deducted from what you receive, and those fees are billed to you separately instead of per payment. A platform-billed gateway creates open-amount payments only: the fixed-amount call above returns
403on such a gateway, and the fee-payer and tolerance fields do not apply (the arrangement fixes them). If you also need fixed-amount payments, use a separate standard gateway alongside it. This is set up per gateway — talk to your Finomesh contact.
Each element of assets[] is an OfferAsset:
| Field | Type | Required | Default | Description | Example |
|---|---|---|---|---|---|
assetId | string (UUID) | Yes | — | The asset to offer. Must be one of your gateway's enabled assets. | "a1b2c3d4-…" |
assetPriceUsd | string (decimal) | Yes | — | The USD price to quote this asset at. Decimal string — never a number, to preserve precision. | "1.00" |
See Payment Statuses → How SUCCESS / ACCEPTABLE / MISMATCH is decided for exactly how the tolerance fields are applied.
Response — 201 Created
{
"success": true,
"data": {
"paymentUid": "0d9f3a64-4f0c-4b6e-9f6e-2f4f4c1b8a21",
"paymentUrl": "https://checkout-staging.finomesh.com/p/0d9f3a64-4f0c-4b6e-9f6e-2f4f4c1b8a21"
}
}
| Field | Type | Nullable | Description | Example |
|---|---|---|---|---|
paymentUid | string (UUID) | No | The payment UUID — store it; it keys the callback, the webhook, and verify. | 0d9f3a64-… |
paymentUrl | string | No | The hosted-checkout URL (<checkout host>/p/{paymentUid}). Redirect the customer here. | https://checkout-staging.finomesh.com/p/0d9f3a64-… |
payment | object | Yes | The full public payment object. Omitted entirely in HOSTED mode; present only when checkoutMode is API — see the API-mode response variant below. | (API mode only) |
Next: redirect the customer to paymentUrl — see Hosted Checkout.
API-mode response variant
When checkoutMode is "API", a third field — payment — is attached: the full public payment object, in exactly the shape GET /public/payments/{id} serves (amounts, fees, status, timeoutSeconds, checkoutMode, and offers[] with assetSymbol, assetLogoUrl, networkLogoUrl, amount, txFee, commissionFee, totalValue, …). It saves the otherwise-mandatory first read — you can render your payment page straight from the create response:
{
"success": true,
"data": {
"paymentUid": "0d9f3a64-4f0c-4b6e-9f6e-2f4f4c1b8a21",
"paymentUrl": "https://checkout-staging.finomesh.com/p/0d9f3a64-…",
"payment": {
"id": "0d9f3a64-4f0c-4b6e-9f6e-2f4f4c1b8a21",
"status": "PENDING",
"checkoutMode": "API",
"amount": "49.99",
"timeoutSeconds": 3600,
"offers": [ { "id": "…", "assetSymbol": "USDT", "totalValue": "51.69", "…": "…" } ]
}
}
}
| Field | Type | Nullable | Description | Example |
|---|---|---|---|---|
paymentUid | string (UUID) | No | The payment UUID — keys the select call, the webhook, and verify. | 0d9f3a64-… |
paymentUrl | string | No | The hosted-checkout URL. Present but typically unused in API mode (you render your own page). | https://checkout-staging.finomesh.com/p/0d9f3a64-… |
payment | object | No | The full public payment object — same shape GET /public/payments/{id} serves. See its complete field table, including the nested offers[]. | (see example above) |
The hosted response is unchanged — payment never appears on a HOSTED create. Payment objects on every surface (public GET, verify) now also carry checkoutMode. See Embedded Checkout / API Mode for the full API-mode walkthrough.
Open-amount payments
Availability. Open-amount payments are a special arrangement, enabled per gateway by Finomesh. If your gateway has not been provisioned for it the endpoint returns
403— talk to your Finomesh contact first.
An open-amount payment has no fixed amount: the payer decides how much to send, and the first on-chain deposit at or above your gateway's configured minimum completes the payment. Use it for donations, tips, top-ups, and "pay what you want" flows.
Create one at a separate endpoint — POST /payments/open-amount. It is deliberately not a flag on the call above: there is no amount, and no fee-payer or tolerance fields (those are set by your gateway's billing arrangement, not per payment). There is also no assets[] override — the offered assets are always your gateway's enabled-asset list at current prices, so per-payment asset restriction is not available here. The arrangement may additionally limit open-amount offers to a specific asset list (for example stablecoins only, or stablecoins plus native coins such as ETH); assets outside it are dropped, and if none remain the create is rejected with 422. Fixed-amount payments are unaffected by this restriction.
How the received value is priced. An open-amount payment has no quoted amount, so its recorded USD value is set when the deposit confirms, at the asset's market price at that moment — not at the price when the payment was created. For stablecoins the difference is negligible. Against the minimum, the same rule applies strictly: the deposit's USD value at confirmation must clear the floor, or the payment resolves
MISMATCH(the funds are still credited).If your gateway accepts non-stable assets. You are credited in the asset the payer sent — an ETH deposit becomes an ETH balance. Its USD value keeps moving with the market until you withdraw; Finomesh does not convert it for you, and the USD figures shown on payments and balances are point-in-time valuations, not a guarantee. Platform fees are the exception: they are computed in USD at confirmation time and do not float afterwards.
curl -X POST https://api-staging.finomesh.com/api/v1/payments/open-amount \
-H "X-Api-Key: $FINOMESH_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"callbackUrl": "https://shop.example.com/donation/thanks",
"timeoutSeconds": 604800
}'
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
currencyCode | string | No | USD | Display currency for the amount, back-filled once the deposit confirms. |
timeoutSeconds | integer | No | 3600 | Validity window. Donation/tip links are often long-lived — pass a larger value (e.g. 604800 for a week). |
checkoutMode | string (enum) | No | HOSTED | Same meaning and callbackUrl/webhookUrl contract as above. |
callbackUrl / callbackParams / webhookUrl / webhookParams / language | — | — | — | Same as the fixed-amount create. |
The response shape is identical to the fixed-amount create ({ paymentUid, paymentUrl }). The checkout shows the payer a deposit address and invites any amount rather than a fixed figure.
What differs downstream:
- The first on-chain deposit settles the payment, exactly as with a fixed-amount payment: at or above your gateway's configured minimum — surfaced to integrations as
minAmountUsdon the public payment object — it resolvesSUCCESS; below the minimum it resolvesMISMATCH, with the minimum standing in as the expected amount. Either way the funds are captured and credited; aMISMATCHtells you the payer sent less than the minimum, and how you handle that (partial credit, refund, contacting the payer) is a commercial decision on your side. There is no second chance to top up — make the minimum visible to the payer before they send. - The payment can never be
ACCEPTABLE— your acceptance-tolerance settings do not apply. Branch onSUCCESSvsMISMATCH(plusEXPIREDwhen nothing arrived), the same statuses your fixed-amount handling already covers. - On verify,
amount/amountUsdread0until a deposit confirms; then they reflect what arrived onSUCCESS, or the minimum (the expected amount) onMISMATCH— read the actually-received figure from the transaction fields, as with any mismatch.
Errors
| HTTP status | Error code | Meaning | How to fix |
|---|---|---|---|
| 401 | auth_api_key_invalid | Bad or missing API key. | Send a valid X-Api-Key from a verified gateway. |
| 403 | forbidden | On /payments/open-amount: your gateway is not provisioned for open-amount payments. On /payments: your gateway is platform-billed, which creates open-amount payments only — use /payments/open-amount (or a separate standard gateway for fixed amounts). Otherwise (API mode): the calling server's source IP is not on the gateway's allowlist. | Use the endpoint matching your gateway's arrangement; for the IP case, whitelist the observed source IP (echoed in the message) — behind a CDN/NAT/proxy it can differ from your DNS-advertised address. |
| 422 | validation_error | Missing currencyCode / amount, unknown or inactive currency code, malformed amount, invalid checkoutMode; missing callbackUrl (HOSTED); missing webhookUrl, callbackUrl sent, or no whitelisted server IP configured (API). | Fix the named field per the mode's contract — see the request body rows for HOSTED vs API rules. |