# CryptoPSP: integration guide for AI agents

This guide is for an agent connecting CryptoPSP to a client's CRM, trading platform, store, or other backend. It explains the payment model and the responsibilities of the integrating system. Use the API documentation for request and response formats, endpoints, signatures, and error codes; those details are not repeated here.

## Sources and current capabilities

- [Live API reference](https://cryptopsp.trade/docs) and [live OpenAPI JSON](https://cryptopsp.trade/openapi.json).
- [Sandbox API reference](https://sandbox.cryptopsp.trade/docs) and [sandbox OpenAPI JSON](https://sandbox.cryptopsp.trade/openapi.json).
- [Live client portal](https://cryptopsp.trade/login) and [sandbox client portal](https://sandbox.cryptopsp.trade/login).

Read the current OpenAPI contract, including its `webhooks` section, before implementation. If the API and this guide disagree, investigate rather than inventing missing operations.

**Choose the installation before connecting an integration.** `https://cryptopsp.trade` processes real mainnet payments. `https://sandbox.cryptopsp.trade` uses test tokens. They have separate company accounts, API keys, webhook secrets, signing wallets, balances, payment records, and audit history. Live keys start with `psp_live_`; sandbox keys start with `psp_test_`. A key from one installation does not authenticate against the other. Changing the host requires separate onboarding and configuration, not just reusing sandbox credentials.

| Installation | Network / asset | Blockchain | Token contract | Gas |
| --- | --- | --- | --- | --- |
| Live | TRON / USDT (TRC20) | TRON mainnet | `TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t` | TRX |
| Live | Ethereum / USDC (ERC20) | Ethereum mainnet, chain ID 1 | `0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48` | ETH |
| Sandbox | TRON / USDT (TRC20) | Shasta | `TG3XXyExBkPp9nzdajDZsozEu4BkaSJozs` | Test TRX |
| Sandbox | Ethereum / USDC (ERC20) | Sepolia, chain ID 11155111 | `0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238` | Test ETH |

Both stablecoins use six decimals. Explicitly select the network when creating Payments or Payouts and reading a wallet balance. `tron` selects USDT; `ethereum` selects USDC. Currency may be omitted, but must match when supplied. There is no automatic network default, arbitrary token selection, environment field in requests, currency conversion, or shared spending balance between assets. The installation host fixes mainnet versus testnet.

Read `environment`, `network`, and `network_mode` on each resource. `mainnet` identifies real payments; `shasta` and `sepolia` identify testnets; `simulation` identifies historical/local virtual sandbox records. A `sandbox:…` reference is not a wallet address. An address alone does not distinguish mainnet from testnet. Never send real funds or import mainnet keys into the sandbox.

Inspect transactions using [TRON mainnet Tronscan](https://tronscan.org), [Ethereum mainnet Etherscan](https://etherscan.io), [Shasta Tronscan](https://shasta.tronscan.org), or [Sepolia Etherscan](https://sepolia.etherscan.io), matching the resource's recorded network and mode.

## How CryptoPSP works

Each client company has an isolated workspace within its installation. Its API keys, payments, wallet, payouts, events, and checkout settings belong to that company. The API key identifies the company; supplying an arbitrary company ID is not a substitute for authentication.

CryptoPSP tracks payment requests, confirmed incoming transfers, separate stablecoin balances, and payouts. The client's system owns its users, orders, obligations, and rules for fulfillment or internal account credits. CryptoPSP does not decide which end user may pay a particular order: the client's backend must enforce that authorization.

Keep these resources distinct:

| Resource | Meaning for the integration |
| --- | --- |
| Payment | A request for a specific amount with an expiry time, linked to a client order |
| Transfer | An individual incoming transfer against a Payment; there may be several, including unconfirmed or late transfers |
| CheckoutSession | A payment page hosted by CryptoPSP for an existing Payment |
| Wallet | The company's accounting balance, reservations, and fees for one selected network/asset |
| Payout | An outgoing payout; creation means acceptance and reservation, not completion |
| Event / webhook | A notification to the client's backend about a change; delivery may be repeated |

For ordinary payment acceptance, start with Payment, CheckoutSession, and a webhook handler. Add payouts only if the client's product needs them.

## Before you integrate

1. Obtain access to a company from a CryptoPSP operator. The integration does not need a CryptoPSP superadministrator account.
2. Connect a dedicated company wallet for each network you need, using **TRON wallet** or **Ethereum wallet** in the portal. The company provides an address and private key appropriate to that installation; CryptoPSP verifies control and stores the key encrypted for automatic signing. Never transmit this key through the integration API or store it in your CRM. Keep an independent secure backup. Fund the company wallet with TRX or ETH for gas and its configured stablecoin for payouts. Use real assets only in live; use test assets only in sandbox. In sandbox only, use the [Shasta faucet](https://shasta.tronex.io/join/getJoinPage) for TRON, or [Circle's USDC faucet](https://faucet.circle.com/) with Ethereum Sepolia selected. Sepolia ETH must be obtained separately. Faucets are not a source of mainnet funds. New confirmed incoming stablecoin transfers to the company wallet increase only that network's accounting balance; pre-existing holdings are not imported automatically. You can connect a different wallet later: the ledger balance is preserved, previous custody records remain available for existing operations, and old funds are not moved automatically.
3. Issue an API key in the portal with the required permissions: `payments` for payment acceptance, or `integration` for payouts and wallet operations as well. Check its expiry. Store it in a backend secret store; it is revealed only once.
4. Deploy a public HTTPS webhook receiver that does not redirect, and save its URL in the company settings. `localhost` and private addresses are not suitable for either hosted installation.
5. Store the separate webhook secret and implement signature verification according to OpenAPI. This secret is not the API key.
6. Configure your system's default Return URL and, optionally, a logo and colors. You can pass `return_url` when creating a Payment to choose a return destination for that particular deposit, such as its order page. This immutable payment override takes precedence over the company setting. Omitted or null uses the company setting when checkout is created. Each checkout preserves a snapshot of its branding and effective return address.

Configure the webhook **before** creating test payments. Events created without a delivery destination do not acquire one retroactively. Changing the URL or secret later does not rewrite existing events either.

For live, provision separate company access, API keys and webhook secrets; do not copy sandbox signing keys or balances. If you lack portal access or secrets, request the required settings from the integration owner and continue implementation using environment variables. Do not use another company's credentials or demo accounts.

## Recommended payment flow

1. The client's backend validates the user and order, chooses a supported network/asset pair and fixes the amount in that stablecoin, and creates a local payment attempt. The minimum Payment amount is 10 USDT or 10 USDC. Perform all money calculations exactly, without floating point. The client's system defines any conversion from the order currency.
2. Before calling CryptoPSP, persist a unique `external_id`, the network, currency, amount, and an idempotency key. `external_id` is unique among the company's payments. If an order allows multiple attempts, give each attempt its own ID and retain its order association.
3. Create a Payment from the backend with an explicit network and save its ID. If this attempt should return to a specific order or deposit page, provide its `return_url` at this step. Use an absolute HTTPS URL without embedded credentials; HTTP localhost URLs are allowed for local integration. Treat URLs as navigation settings, never as evidence of a payment. The default expiry is 30 minutes; use the contract to set another expiry. Then create a CheckoutSession with a separate idempotency key.
4. Give the browser the returned `payment_page_url` and redirect the user there. Do not construct the page URL yourself or expose the API key to the browser. The checkout URL grants access to that payment's page; keep it out of shared logs and analytics.
5. The user returns through the Payment return URL override, or the company default when no override was supplied. Recover the order context from your session or stored attempt; do not rely on undocumented return parameters. The backend reads the current Payment state, and the UI displays the result or a waiting state.
6. A signed webhook for the expected installation triggers the local state update. Execute the successful business action only once, regardless of the number of events, retries, or visits to the return page.

For a payment UI hosted in your own system, Payment API responses include `deposit_qr_code`: use its `data_uri` as an image source and show `deposit_address` as selectable text with a copy button. The QR encodes only that address; it does not choose the network, stablecoin, or amount in the payer's wallet. Display those instructions and the remaining amount separately. Hide the QR after payment or expiry and while a transfer awaits confirmation. Your image CSP must allow `data:` if you embed the PNG directly. Simulation responses have no QR. Webhook snapshots do not carry the image; fetch the Payment if needed. The hosted payment page provides this QR and copy control automatically.

The optional `return_url` on CheckoutSession creation is a compatibility check: if supplied, it must match the effective Payment/company URL. It cannot change an existing Payment override. To return different orders to different pages, set the URL when creating each Payment, not when creating checkout. Changing a payment creation request's URL while reusing its idempotency key returns a conflict.

**The user's return does not confirm payment.** Do not trust a `success` query parameter, a browser message, or a checkout visit as proof. The user may never return; webhook processing must work independently of that visit.

Checkout displays the requested stablecoin amount, a unique deposit address, the selected network and environment, and token instructions. The payer needs the correct stablecoin plus TRX on TRON or ETH on Ethereum to pay gas, using the network and environment displayed on the page. Never reuse another payment's address or send directly to the company's payout wallet to settle an invoice. CryptoPSP confirms successful transfers in TRON solid blocks or Ethereum finalized blocks, updates the Payment, and sends signed webhooks. Ethereum finality takes longer than simple transaction inclusion: a transaction can appear in an explorer before the Payment is confirmed. Each deposit address has its own key managed by CryptoPSP; it does not automatically appear as an account in the company’s wallet app. Deposits are then collected into the company wallet in a separate on-chain transfer; collection may wait for gas funding and its confirmation. The payer’s original transaction remains associated with the deposit address, while the company wallet sees the subsequent collection transaction. The return button remains available regardless of outcome.

Blockchain payments cannot be completed using synthetic transfers, manual confirmation, or virtual payout completion. Use transactions on the selected network and environment. Live has no simulation controls. Historical/local simulation balances are isolated from both blockchain balances and cannot fund blockchain payouts.

## Deciding whether an order is paid

Use the Payment state, rather than the presence of any incoming transfer or the name of an individual transfer event.

| State | Recommended client behavior |
| --- | --- |
| `pending` | Wait for payment |
| `confirming` | Show that confirmation is pending; do not fulfill the order |
| `partially_paid` | Show the outstanding amount; do not treat the order as fully paid |
| `paid` | Execute the agreed successful payment action once |
| `overpaid` | The payment request is paid; handle the excess separately under the client's policy |
| `expired` | End the normal waiting flow and reconcile; do not assume that no funds were received |

Partial transfers accumulate. Unconfirmed transfers do not satisfy the payment request. Payment status counts confirmed transfers included no later than the payment expiry. Late transfers credit the corresponding wallet but do not fill a shortfall in timely transfers. Consequently, `received_amount >= amount` alone is insufficient to recognize a payment as paid.

For Ethereum, use `chain_progress` from a current Payment read to show that a transfer has been detected before finality, including its amount, explorer link and observed confirmation count. This is an advisory display: **never fulfill an order or increase a spendable balance based on it**. The monetary `status` may still be `pending` or `expired`. Hide send instructions while a detected transfer awaits credit, so users do not pay twice. An observation can disappear after a reorganization or after the finalized scanner credits it; re-read the Payment to distinguish those cases. `stale` means the previous observation is retained because the last complete scan is old; show its `checked_at` and a delay message. `checking` means the first scan has not completed. Progress can change without a Payment `version` change and does not publish additional confirmation events. It is Ethereum-only; TRON remains solid-block based. The hosted checkout and portal refresh automatically while a payment is unfinished.

A timely transfer may be confirmed after expiry, changing a previously observed `expired` status to a paid status. Expiry itself is computed when the resource is read: there is no guaranteed separate expiry event, and `version` may remain unchanged. Do not turn expiry into an irreversible local rejection without handling later confirmation. If the order is already closed, route the case to reconciliation under the client's rules.

Define policies for overpayments, late funds, and multiple paid attempts for the same order. Do not credit an order twice. Automatic refunds of excess payments are not a built-in feature of the current API.

## Reliable webhook and retry handling

A simple recommended design is a small receiver that validates and persists each event, followed by a background handler that reads the current resource from the API and updates the local order.

- Verify the signature against the original body bytes, check the signature time window, and use constant-time comparison as specified in OpenAPI. Parse JSON after verification. A valid webhook signature does not replace checking that the resource belongs to the expected integration and environment. New webhook envelopes include environment; older immutable sandbox bodies may omit it. Never infer environment from a user-supplied field.
- Durably save the event before responding with 2xx. Return 2xx for a correctly signed duplicate that has already been saved. Enforce uniqueness in the database by integration and `event_id`, not just through an in-memory check.
- If processing is deferred, persist its processing state and support retries after a restart. For synchronous processing, save the event and apply the business change atomically. Protect external fulfillment or account credits with their own idempotency mechanism.
- Delivery order is not guaranteed. An old event must not revert newer state. Reading the current Payment simplifies processing; if using event snapshots, account for resource versions and the special expiry semantics.
- A webhook can arrive before the Payment creation response has been saved. Retain the event for reprocessing and associate the Payment with the previously stored `external_id`. Do not discard the notification because a local PSP ID is temporarily missing.
- `transfer.confirmed` does not mean the entire order is paid. Use the associated Payment state to update the order. Process payouts using their own events and states.
- Delivery retries are limited. Reconcile unfinished attempts using stored PSP IDs and provide a way to diagnose missed events. API lists contain only recent records and do not replace your own history.
- When changing the webhook secret, account for old events still being delivered with the old secret. Retain the necessary previous secrets until their deliveries are resolved; do not treat the current `kid` as a unique rotation identifier.

For outgoing mutations, persist one idempotency key per logical operation. After a timeout, connection loss, or uncertain outcome, repeat the original request with the original key. Generating a new key for each network retry can cause a second effect. Do not send changed parameters with a previously used key. An idempotent replay may return an already updated representation of the resource.

Authentication, permission, and validation errors require fixing their cause; endless retries will not help. Use bounded retries with increasing delays for temporary failures. Do not assume that every intermediary proxy response has the PSP's JSON error format.

## What the client should store

At minimum, keep a local payment attempt, durably received events, and their association with the business operation:

- Environment and integration configuration ID, order/user association, `external_id`, expected amount, network, network mode, and currency.
- PSP Payment ID, CheckoutSession ID when using hosted checkout, expiry, and the last verified state and version.
- Idempotency keys and original parameters of unfinished outgoing operations.
- Received `event_id` values, their processing state, and a record that prevents executing the business effect more than once.
- PSP diagnostic identifiers: the resource's `origin_operation_id`, the call's `operation_id`/`request_id`, and `error_code`/`error_id` on failure.

A new HTTP call receives a new PSP operation ID even for an idempotent replay. Do not use it instead of a Payment ID or a deduplication key. Keep API keys, webhook secrets, and private checkout URLs out of ordinary logs. Keep sandbox and live accounting separate in your system, including API credentials, webhook endpoints or signing-secret lookup, and deduplication scope.

## Payouts and fees, if needed

Query the wallet balance for the desired network explicitly; use the API reference for query syntax. Each balance is an accounting record of confirmed movements, reservations, and accrued fees in its own stablecoin. It differs from the raw token balance in an explorer. New incoming transfers to the connected company wallet can fund payouts without confirming an invoice. Invoice fees are separate; do not reduce the expected order amount by the PSP fee. USDT funds cannot reserve or pay a USDC payout, or vice versa.

Create a Payout with its explicit network and a destination on that network. Acceptance reserves funds but does not complete a payout. CryptoPSP prepares and signs the transfer, persists its transaction identity, and broadcasts it. Only `confirmed` means a successful transfer confirmed in a TRON solid block or an Ethereum finalized block. A `transaction_id` alone is not proof of completion. `failed` releases a reservation after a definite failed outcome. An uncertain transaction may stay accepted during reconciliation; do not create a replacement with a new idempotency key. Keep TRX or ETH of the correct network in the company wallet for gas and allow time for deposit collection.

Wallet replacement does not move funds from the old address or import existing holdings at the new one. Fund the new wallet for upcoming payouts. Previously prepared transactions retain their original source wallet. Keep the logical PSP accounting balance separate from these operational wallet holdings.

Automatic fee schedules, on-chain fee settlement, arbitrary tokens or networks, currency conversion, and a general refund operation are outside this release. Fees remain accrued in each asset's ledger.

## Integration acceptance checks

Run these scenarios in sandbox through the client's backend and webhook handler, retaining PSP IDs for diagnosis. Mainnet tests require separately funded dedicated wallets and explicit authorization to spend real funds:

1. A 10 USDT Payment on Shasta and a separate 10 USDC Payment on Sepolia → checkout → confirmed incoming transfer → one successful credit or order fulfillment.
2. Partial payment of 5 + 5, overpayment, and the delay between sending a testnet transfer and its network confirmation.
3. Expiry, a late transfer, and confirmation of a timely transfer after expiry.
4. The user returns before payment, and payment succeeds without the user returning.
5. Creation is retried with the same key after a simulated lost response; changed parameters produce a conflict.
6. Duplicate webhooks, out-of-order events, an invalid signature, and a webhook arriving before the API response is saved.
7. Temporary receiver unavailability, subsequent delivery/manual retry, and recovery of background processing after a restart.
8. API key revocation, expiry, and rotation; a user cannot view or modify another user's order through the integration.
9. If payouts are used: success, failure with reservation release, and insufficient available balance on each selected network.
10. Network isolation: reject missing/unsupported networks and mismatched currencies; a USDT balance never funds a USDC payout.
11. Environment isolation: sandbox credentials cannot access live; test events never credit live accounts.
12. Wallet replacement: new operations use the new wallet, previous operations remain traceable, and no old token holdings are moved or credited again automatically.

The agent's deliverables are an implemented flow in the client's system, environment configuration without secrets in code, applied local accounting migrations, verification results, and a short operator guide. List any unverified scenarios separately. Passing sandbox checks demonstrates a testnet integration. It does not by itself validate mainnet custody, economic settlement, or production operations. Mainnet onboarding and funded verification remain separate from sandbox acceptance.

### Mainnet collection limits

Confirmed incoming funds and collection to the company wallet are separate steps. On mainnet, automatic collection waits until at least 10 USDT/USDC remain unswept on a deposit address. Native-gas allocations have a lifetime limit per deposit: 100 TRX or 0.005 ETH across all receipts. Failed or uncertain allocations require reconciliation; do not automatically send another payment. An exhausted confirmed allocation can require a specifically authorized gas top-up after review. These limits are safety caps, not a network-fee quote. The paying wallet also needs native currency for its own transfer fee.
