Build in-app transaction
Build the unsigned transaction that pays an in-app invoice from the customer's linked wallet — Solana or EVM (Ethereum / BSC / Polygon) — ready for the wallet to sign.
https://triport.io/v1/billing/invoices/inv_8f2a1c9d4e6b40f1/inapp/build- Network
- Solana / Ethereum / BNB Smart Chain / Polygon (by the invoice's rail)
- Auth
- Session cookie (console login)
Builds the transaction that pays a crypto-inapp invoice from the customer's
own wallet. You pass the wallet address as payer; the backend routes by the
rail's family (see GET /v1/billing/rails) and returns a
transaction for the remaining amount due, addressed to the invoice's deposit
address:
| Rail family | Rails | What you get | What the wallet does |
|---|---|---|---|
sol | sol-native, sol-spl-usdc, sol-spl-usdt | transaction_b64: unsigned legacy tx (SystemProgram::Transfer or SPL TransferChecked + invoice reference key; creates the recipient token account if needed) | sign and send (signAndSendTransaction), or sign only and submit signed_b64 |
evm | eth-*, bsc-*, polygon-* (USDT/USDC) | {chain_id, to, data, value} — an ERC-20 transfer(deposit, amount) call | switch to chain_id, then eth_sendTransaction({from: payer, to, data, value}) |
Tron is not paid in-app. tron-usdt is offered only as an on-chain
transfer (crypto-onchain: send USDT to the invoice's deposit address); its
inapp_build is false, so build / submit answer
400 inapp_rail_unsupported for it.
Linked payer only. payer must be the account's currently linked wallet of
that family (see GET /v1/auth/wallets); any other address
gets 403 payer_not_linked before anything is built. Link the wallet first
(wallet/link); a linked wallet can be changed only by
support (self-service replace was removed on 2026-09-29).
Building does not sign, send or settle anything. After the wallet signs (and
possibly sends), call POST …/inapp/submit. The invoice
turns paid only when the deposit watcher sees the transfer on chain with the
rail's confirmations — follow it on the
invoice event stream.
Gate the feature on the rail's inapp_build flag; rails where it is false
answer 400 inapp_rail_unsupported (or 503 inapp not configured when in-app is
off entirely).
Parameters
Path parameters
idstringrequiredpending, not expired, and use the crypto-inapp channel.Request bodyobjectpayerstringrequired0x… (EIP-55 checked if mixed case). Must be the linked wallet of the rail's family and differ from the deposit address.Response
Solana (family: sol)
| Field | Type | Description |
|---|---|---|
transaction_b64 | string | Base64 unsigned legacy transaction. |
blockhash | string | Recent blockhash used. |
blockhash_valid_until | string (RFC 3339) | Conservative expiry (~90 s). After it, build again instead of submitting. |
chain_id, chain_id_hexnumber, string1, 56, 137. Switch the wallet (wallet_switchEthereumChain) and verify eth_chainId before sending.to, data, valuestringtransfer calldata, 0x0. Gas is estimated by the wallet.amount_unitsstringdata = remaining amount due, in token base units (decimal string).invoice_amount_unitsstringcredited_unitsstringdecimalsnumberdeposit_address, payerstringexpires_atstring (RFC 3339)simulationstringok, transfer_reverted (e.g. blacklisted or paused token — informational), unavailable.Errors
EVM returns stable codes in {"error": "<code>", …}. Solana returns
free-form strings that start with the code (e.g. "invoice_not_pending: paid") —
compare by prefix.
| Code | HTTP | When it happens |
|---|---|---|
auth required | 401 | No valid session. |
invoice not found | 404 | No such invoice, or it belongs to another user. |
inapp_rail_unsupported | 400 | In-app is disabled for this rail's family (body carries rail). |
invoice_not_inapp, rail_not_solana_inapp / rail_not_evm_inapp | 400 | Wrong channel or rail. |
bad json…, payer required, bad payer address, payer_equals_recipient | 400 | Invalid payer. |
payer_not_linked | 403 | payer is not the linked wallet of the family. Body: { "error", "family", "hint": "link_or_replace", "message" }; message asks the user to pay from the linked wallet and to contact support to change it (the hint value is kept for compatibility). |
invoice_not_pending | 409 | The invoice is no longer pending. |
invoice_already_funded | 409 | EVM: the full amount is already credited while the invoice is still settling. Body adds invoice_amount_units, credited_units. |
invoice_expired | 410 | The invoice expired. |
insufficient_token_balance | 422 | EVM: the payer holds less than amount_units. Body adds token, decimals, balance_units, amount_units. |
build_failed: …, rpc_unavailable: … | 502 / 503 | Solana: upstream RPC failed. |
payer_check_unavailable | 503 | The linked-wallet check could not run; retry (Retry-After: 2). |
billing_unavailable | 503 | Billing is temporarily unavailable on this node (failover or maintenance); retry after Retry-After. |
inapp not configured | 503 | In-app payments are disabled on this deployment. |
See Errors for the shared envelope.
Examples
JavaScript (fetch) — EVM
const res = await fetch(`https://triport.io/v1/billing/invoices/${invoiceId}/inapp/build`, {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ payer }),
});
const b = await res.json();
if (!res.ok) throw new Error(b.error);
await provider.request({ method: 'wallet_switchEthereumChain', params: [{ chainId: b.chain_id_hex }] });
if ((await provider.request({ method: 'eth_chainId' })) !== b.chain_id_hex) throw new Error('wrong_chain');
const txHash = await provider.request({
method: 'eth_sendTransaction',
params: [{ from: payer, to: b.to, data: b.data, value: b.value }],
});
// then POST …/inapp/submit { tx_hash: txHash }Notes
- Remaining amount. After a partial payment, build asks only for what is
still due (
amount_units); the watcher credits partial payments as they land. - Payments from other wallets (e.g. a manual transfer to the deposit address)
are still credited, but flagged: the invoice then shows
payer_linked: false(see Get invoice). - Settlement is asynchronous and needs the rail's
confirmations(eth 12, bsc 15, polygon 30, sol 1).