Public payment endpoints
These endpoints power the hosted checkout — and, for API checkout mode, your own embedded payment page. They need no API key. For hosted payments, knowledge of the payment UUID is the credential (which is why payment IDs are unguessable UUIDs and cross-gateway probing always 404s). In hosted mode you normally never call them yourself; in API mode your server calls the select endpoint (and optionally expire) directly, and polls status with statusOnly=true. The sandbox simulate endpoint is also useful in automated tests.
For a payment created with checkoutMode: "API", every endpoint on this page — the read, expire, select, and sandbox simulate — requires the caller's source IP to be on the gateway's allowlist (403 forbidden otherwise). API-mode payments are only reachable from whitelisted server IPs — the payment UUID alone reveals nothing. Hosted payments are unaffected.
GET /public/payments/{id}
The redacted payment projection the checkout renders from. The response is mode-aware: hosted payments always get the full payment object; API-mode payments are source-IP gated and can opt into a slim status object via statusOnly — without it, the full payment object is returned whatever the status.
curl https://api-staging.finomesh.com/api/v1/public/payments/0d9f3a64-…
{
"success": true,
"data": {
"id": "0d9f3a64-4f0c-4b6e-9f6e-2f4f4c1b8a21",
"gatewayId": "9b1de2c8-7a41-4c8e-b3f2-6d5e4a3b2c1d",
"currencyId": "3f8a6c1e-2b4d-4e6f-8a9b-1c2d3e4f5a6b",
"currencyCode": "USD",
"amount": "49.99",
"amountUsd": "49.99",
"status": "PENDING",
"type": "DIRECT",
"checkoutMode": "API",
"isSandbox": false,
"timeoutSeconds": 3600,
"expiresAt": "2026-07-03T12:10:00Z",
"txFeePayer": "CUSTOMER",
"commissionFeePayer": "CUSTOMER",
"overAcceptableDiff": "0.5",
"underAcceptableDiff": "0.5",
"acceptableDiffKind": "PERCENTAGE",
"checkAmountDeviation": true,
"offers": [
{
"id": "7c2e1f30-5d6a-4b8c-9e0f-1a2b3c4d5e6f",
"assetId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"assetSymbol": "USDT",
"assetNetwork": "TRON",
"assetType": "TRC20",
"assetLogoUrl": "https://files.finomesh.com/assets/usdt.png",
"networkLogoUrl": "https://files.finomesh.com/networks/tron.png",
"assetPriceUsd": "1.00",
"amount": "49.99",
"txFee": "1.20",
"commissionFee": "0.50",
"totalValue": "51.69",
"networkFeeNative": "13.6",
"networkFeeUsd": "1.20",
"feeNativeSymbol": "TRX"
}
],
"createdAt": "2026-07-03T10:21:43Z",
"updatedAt": "2026-07-03T10:21:43Z"
}
}
It contains the amounts, status, checkoutMode, offers, and — once the payment resolves on-chain — the resolving txHash, receivedAmount, and a ready-to-use explorerTxUrl block-explorer link. It deliberately excludes merchant-only fields: webhookUrl, webhookParams, webhookDispatched, and internal identifiers. (callbackUrl and callbackParams are present for hosted payments — the callback is a browser redirect, so its target is payer-facing by definition.)
Nothing on this surface lets a payer affect what you get paid: it is read-only state plus the narrowly-scoped POSTs below.
Query parameters:
| Parameter | Type | Required | Default | Description | Example |
|---|---|---|---|---|---|
statusOnly | boolean | No | false | API mode only — ignored for hosted payments. When true, returns the slim status object instead of the full projection — the recommended cheap status-inquiry poll. A non-boolean value returns 422 validation_error. | true |
The tables below document the merchant-facing contract. Responses for hosted payments may additionally carry presentation fields consumed only by the hosted checkout UI (theming, payer-inquiry flags, per-offer explorer templates); they are not part of the merchant contract and are omitted entirely for checkoutMode: "API" payments.
API-mode behavior
For payments created with checkoutMode: "API" this endpoint differs in two ways:
- Source-IP gated. The same gate as the select endpoint: the caller's IP must be on the owning gateway's allowlist, else
403forbidden. An API-mode payment is completely invisible to anyone but your whitelisted servers — even with the UUID. statusOnlyopt-in. With?statusOnly=trueyou get the slim status object — the recommended cheap status-inquiry poll. Without it, the full payment object is returned whatever the status (minus the redacted fields above), so you can re-fetch everything about the payment at any point in its life.
The recommended pattern: poll with ?statusOnly=true as your internal status inquiry — a tiny, stable payload — and fulfil only from verify, the authoritative read.
curl "https://api-staging.finomesh.com/api/v1/public/payments/{paymentId}?statusOnly=true"
Slim status object
{
"success": true,
"data": {
"paymentUid": "0d9f3a64-4f0c-4b6e-9f6e-2f4f4c1b8a21",
"status": "SUCCESS",
"isVerified": false,
"verifiedAt": null
}
}
| Field | Type | Nullable | Description | Example |
|---|---|---|---|---|
paymentUid | string (UUID) | No | The payment UUID. | 0d9f3a64-… |
status | string (enum) | No | One of PENDING, SUCCESS, ACCEPTABLE, MISMATCH, EXPIRED. | SUCCESS |
isVerified | boolean | No | Whether you have already called verify on this payment. Reading it here never flips it — only verify does. | false |
verifiedAt | string (RFC 3339) | Yes | When the payment was first verified. null until then. | null |
Errors
| HTTP status | Error code | Meaning | How to fix |
|---|---|---|---|
| 403 | forbidden | API-mode payment and the caller's source IP is not on the gateway's allowlist. | Whitelist the observed source IP echoed in the message — behind a CDN, NAT, or egress proxy it can differ from your server's DNS-advertised IP. |
| 404 | not_found | Unknown payment. | Check the payment UUID. |
| 422 | validation_error | statusOnly is not a valid boolean. | Send statusOnly=true or statusOnly=false. |
Payment object fields
The object returned by this endpoint — and inlined as payment on an API-mode create.
Monetary values are decimal strings; example values are shown in the JSON sample above.
| Field | Type | Nullable | Description |
|---|---|---|---|
id | string (UUID) | No | The payment UUID. |
gatewayId | string (UUID) | No | The owning gateway's ID. |
currencyId | string (UUID) | No | Internal ID of the invoice currency. |
currencyCode | string | Yes | Invoice currency ISO code (e.g. USD). Omitted if unresolved. |
amount | string (decimal) | No | Invoice amount in currencyCode units. Decimal string — never a number, to preserve precision. |
amountUsd | string (decimal) | No | Invoice amount converted to USD. Decimal string — never a number. |
openAmount | boolean | No | true for an open-amount payment — the payer chooses what to send. When true, each offer's amount / totalValue are 0 placeholders and must not be shown as a figure to send; the checkout invites any amount instead. Always false unless your gateway is provisioned for open-amount. |
minAmountUsd | string (decimal) | Yes | Present only when openAmount is true: the minimum USD value the first deposit must reach for the payment to resolve SUCCESS. The first deposit settles the payment either way — below the minimum it resolves MISMATCH (the minimum is the expected amount; the funds are still credited) — so show this figure to the payer before they send. |
status | string (enum) | No | One of PENDING, SUCCESS, ACCEPTABLE, MISMATCH, EXPIRED. |
type | string (enum) | No | DIRECT or PAYLINK. |
checkoutMode | string (enum) | No | HOSTED or API. |
isSandbox | boolean | No | true for a sandbox payment. |
timeoutSeconds | integer | No | Validity window in seconds. Prefer expiresAt for countdowns; this is the raw window length. |
expiresAt | string (RFC 3339) | No | The payment's authoritative deadline. Drive countdowns from this server-owned value rather than re-deriving createdAt + timeoutSeconds client-side. |
txFeePayer | string (enum) | No | Who bears the network fee: MERCHANT or CUSTOMER. |
commissionFeePayer | string (enum) | No | Who bears the Finomesh commission: MERCHANT or CUSTOMER. |
overAcceptableDiff | string (decimal) | No | Overpayment tolerance. Decimal string — never a number. Read per acceptableDiffKind. |
underAcceptableDiff | string (decimal) | No | Underpayment tolerance. Decimal string — never a number. |
acceptableDiffKind | string (enum) | No | How the tolerances are read: ABSOLUTE or PERCENTAGE. |
checkAmountDeviation | boolean | No | Whether the acceptance band is applied at all. |
callbackUrl | string | Yes | Browser redirect target. Present for HOSTED payments only. |
callbackParams | object | Yes | Arbitrary merchant JSON echoed into the callback redirect. |
txHash | string | Yes | The resolving on-chain transaction hash. Present once the payment resolves. |
receivedAmount | string (decimal) | Yes | Crypto amount actually received. Decimal string. Present once resolved. |
paidOfferId | string (UUID) | Yes | Which offer was paid. Present once resolved. |
explorerTxUrl | string | Yes | Ready-to-use block-explorer link for txHash — the paid offer's chain-explorer URL with the transaction hash already substituted. Present once the payment has an on-chain transaction (statuses SUCCESS, ACCEPTABLE, MISMATCH) and the chain has an explorer configured. Sandbox payments never have one (no real transaction exists). |
language | string | Yes | Checkout display-language hint. |
offers | array<Offer> | No | The payable assets — see the offer object sub-table. |
createdAt | string (RFC 3339) | No | Creation timestamp. |
updatedAt | string (RFC 3339) | No | Last-update timestamp. |
Offer object — offers[] element
One payable asset. The same shape is returned as offer by the select endpoint.
| Field | Type | Nullable | Description |
|---|---|---|---|
id | string (UUID) | No | The offer ID — pass it to the select endpoint. |
assetId | string (UUID) | No | The underlying asset ID. |
assetSymbol | string | Yes | Asset ticker (e.g. USDT). Empty if metadata is unavailable. |
assetNetwork | string | Yes | Chain the asset settles on (e.g. TRON, ETH, BSC). |
assetType | string (enum) | Yes | Token standard: NATIVE, ERC20, TRC20, or BEP20. |
assetLogoUrl | string | Yes | Absolute CDN URL of the asset icon (unsigned, stable). |
networkLogoUrl | string | Yes | Absolute CDN URL of the network icon. |
depositAddress | string | Yes | The address to pay. Populated only after the payer/merchant selects this offer (or another on the same network). |
assetPriceUsd | string (decimal) | No | USD price the asset was quoted at. Decimal string — never a number. |
amount | string (decimal) | No | The invoice principal expressed in the asset (fees excluded). Decimal string. |
txFee | string (decimal) | No | Network-fee portion the customer pays ("0" if the merchant bears it). Decimal string. |
commissionFee | string (decimal) | No | Commission portion the customer pays ("0" if the merchant bears it). Decimal string. |
totalValue | string (decimal) | No | The exact amount the customer must send — principal plus any customer-paid fees folded in. Decimal string. Display this one. |
networkFeeNative | string (decimal) | Yes | Estimated network fee in the chain's native coin. Decimal string. Omitted when the estimate is stale. |
networkFeeUsd | string (decimal) | Yes | Estimated network fee in USD. Decimal string. Omitted when the estimate is stale. |
feeNativeSymbol | string | Yes | Native-coin ticker for networkFeeNative (e.g. TRX, ETH). |
POST /public/payments/{id}/expire
Called by the checkout when its countdown reaches zero (in API mode, your server may call it when your countdown reaches zero). Idempotent and safe-by-construction: it transitions PENDING → EXPIRED only when the payment is genuinely past its deadline — calling it on a still-valid or already-paid payment is a no-op that returns the current state. A hostile caller cannot kill a valid payment.
For API-mode payments the call is source-IP gated like the read and select — an off-allowlist caller gets 403 forbidden before any effect. The response is the same full (redacted) payment object the GET serves, now showing the current status (EXPIRED if the expiry fired). Hosted expire is unchanged.
POST /public/payments/{id}/offers/{offerId}/select
Selects an offer and returns the deposit address and exact amount to pay — the heart of API checkout mode. Hosted payments never use it (the hosted checkout has its own channel); it exists so an embedded merchant's server can drive asset selection over REST.
curl -X POST https://api-staging.finomesh.com/api/v1/public/payments/{paymentId}/offers/{offerId}/select
Path parameters (there is no request body):
| Parameter | Type | Required | Description | Example |
|---|---|---|---|---|
id | string (UUID) | Yes | The payment UUID ({paymentId} in the path). | 0d9f3a64-… |
offerId | string (UUID) | Yes | The offer to select — an id from the payment's offers[]. | 7c2e1f30-… |
No request body and no auth header — but two server-side gates make this stricter than the rest of the public group:
- the payment must have been created with
checkoutMode: "API"and still bePENDING(409otherwise); - the caller's source IP must be on the owning gateway's IP allowlist, configured in the dashboard (
403otherwise) — so the practical caller is the merchant's server, never a payer's browser.
200 OK — address assigned
{
"success": true,
"data": {
"assignmentStatus": "ASSIGNED",
"depositAddress": "TX7k2…",
"amount": "51.69",
"paymentStatus": "PENDING",
"expiresAt": "2026-07-03T11:21:43Z",
"offer": { "id": "…", "assetSymbol": "USDT", "assetNetwork": "TRON", "totalValue": "51.69", "…": "…" }
}
}
The 200 and 202 bodies share one shape — SelectOfferResult:
| Field | Type | Nullable | Description | Example |
|---|---|---|---|---|
assignmentStatus | string (enum) | No | ASSIGNED (address ready — 200) or PENDING (still deriving — 202, re-call after retryAfterMs). | ASSIGNED |
depositAddress | string | Yes | Where the customer must send the funds. Present when ASSIGNED; absent while PENDING. | TX7k2… |
retryAfterMs | integer | Yes | Present on PENDING (202) only — re-call the same endpoint after this many milliseconds. | 400 |
amount | string (decimal) | No | The selected offer's totalValue — the exact amount to send (customer-paid fees folded in). Decimal string — never a number. | 51.69 |
paymentStatus | string (enum) | No | The payment's current status — one of PENDING, SUCCESS, ACCEPTABLE, MISMATCH, EXPIRED (PENDING here). | PENDING |
expiresAt | string (RFC 3339) | No | Deadline — do not display the address past it. | 2026-07-03T11:21:43Z |
offer | object | No | The full selected offer object (symbol, network, logos, fee breakdown) — enough to render the pay screen from this one response. | (see offer table) |
202 Accepted — assignment still in flight
Address derivation happens on demand and can take a moment. On 202 the body carries assignmentStatus: "PENDING" and a retryAfterMs hint — re-call the same endpoint after that delay; the call is idempotent and re-calling is the designed recovery. The address also appears on GET /public/payments/{id} in the offer's depositAddress once assigned. A copy-pasteable retry loop lives in the embedded guide.
Re-selecting
Selecting the same offer again — or a different offer on the same network — returns the same address. Selecting an offer on a different network derives a second address for that network: one address per network, shared by all of its offers.
Errors
| HTTP status | Error code | Meaning | How to fix |
|---|---|---|---|
| 403 | forbidden | Source IP not on the gateway's allowlist. | Whitelist your server's observed source IP (echoed in the message). |
| 404 | not_found | Unknown payment, or the offer is not on this payment. | Check the payment UUID and that offerId is one of this payment's offers[]. |
| 409 | conflict | Payment is not API-mode, is no longer PENDING, or has expired. | Only select on a PENDING payment created with checkoutMode: "API". |
POST /public/payments/{id}/sandbox/simulate
Drives a sandbox payment into a terminal state. Rejected for non-sandbox payments.
For API-mode sandbox payments the call is source-IP gated like select (403 forbidden off-allowlist); hosted sandbox payments keep the browser-driven simulate buttons on the hosted checkout.
curl -X POST https://api-staging.finomesh.com/api/v1/public/payments/{paymentId}/sandbox/simulate \
-H "Content-Type: application/json" \
-d '{ "outcome": "acceptable", "offerId": "{offerId}" }'
| Field | Type | Required | Default | Description | Example |
|---|---|---|---|---|---|
outcome | string (enum) | Yes | — | Which terminal state to drive to: success, acceptable, mismatch, or expire. | "acceptable" |
offerId | string (UUID) | No | — | Which offer was "paid" — lets the simulator synthesize a realistic received amount for the three paid outcomes. Unused for expire. | "7c2e1f30-…" |
Returns the updated public payment projection. All downstream effects (callback, webhook, verify) fire exactly as in production.