# Relay Finance API documentation ## Relay Finance API documentation Source: /docs # Build money movement into your product Relay gives your backend a focused API for accepting payments, assigning reusable deposit wallets to customers, monitoring organization-owned addresses, and receiving signed lifecycle events. The API is designed for server-to-server use. Your organization receives an `accesskey` during onboarding; keep it in a secret manager and never expose it in a browser or mobile application. > **Current production scope:** new payment and wallet work is enabled on TRON and Solana. Always call the active-assets endpoint before presenting a network or token to a customer. Fiat collection currently accepts NGN and settles through the enabled settlement configuration approved for your organization. ## Choose the right collection model | Model | Best for | Address lifecycle | Start here | | --- | --- | --- | --- | | Crypto payment intent | Orders, invoices, or one-off checkouts | A temporary address per intent | [Crypto payment intents](/docs/payments/crypto-payment-intents) | | Fiat payment intent | NGN bank-transfer checkout | A time-limited virtual account | [Fiat payment intents](/docs/payments/fiat-payment-intents) | | Customer deposit wallet | Repeated deposits for a known customer | Reused per customer and network | [Customer wallets](/docs/wallets/customer-wallets) | | External wallet monitoring | Activity on an address your organization already owns | Existing address registered with Relay | [External wallets](/docs/wallets/external-wallets) | ## API conventions - Production base URL: `https://bridge.relayfinance.io` - Requests and responses use JSON unless an endpoint says otherwise. - Authentication uses the `accesskey` request header. - Successful JSON responses use `success`, `data`, and `meta`. - Errors use `success`, `error`, and `meta`. - Every response includes an `X-Request-Id` header. The same value appears in `meta.requestId`. - Payment and wallet monetary values are decimal **strings**. Do not parse them as JavaScript `number` values. The indicative fiat-rate endpoint is the documented exception. - Timestamps are ISO 8601 strings in UTC. ```json { "success": true, "data": { "txId": "RLY-01J9Y7Y9F4M6", "status": "pending" }, "meta": { "requestId": "7ed44f9c-e1ea-4f60-a78f-bf97074dc3ca" } } ``` ## A typical integration 1. Complete Relay onboarding and configure approved settlement destinations in the dashboard. 2. Store your organization API key on your server. 3. Query active payment assets, then create a payment intent or customer wallet. 4. Show the returned deposit instructions to your customer. 5. Receive and verify signed webhooks, deduplicating by event ID. 6. Retrieve the resource when you need the latest authoritative state. Funding and settlement are separate stages. A funded event confirms eligible incoming funds; it does not by itself prove that the outgoing settlement reached its destination. Read `settlementStatus` and the later settlement events independently. ## Documentation formats Every page is available as Markdown through its **View Markdown** action. The complete documentation corpus is also available at [`/docs/llms.txt`](/docs/llms.txt) for development tools and language models. Ready to make a request? Continue to the [quickstart](/docs/getting-started/quickstart). --- ## Quickstart Source: /docs/getting-started/quickstart # Quickstart Create a crypto payment intent from your backend and retrieve it by its Relay transaction ID. ## Before you begin You need: - an active Relay organization; - an organization API key; - an approved settlement destination for the asset, or an external destination address supplied in the request; and - a network and token returned by the active-assets endpoint. Use the production base URL below. Relay may give you a different URL for a controlled test environment. ```text https://bridge.relayfinance.io ``` ## 1. List active payment assets Do not hard-code asset availability. Query Relay when you build or refresh your checkout configuration. ```bash curl --request GET \ --url https://bridge.relayfinance.io/payment/get-active-payment-intents-currency \ --header 'accesskey: YOUR_ORGANIZATION_API_KEY' ``` ```json { "success": true, "data": [ { "name": "Tether USD", "network": "tron", "token": "USDT" } ], "meta": { "requestId": "60739e1b-997e-4bce-bdfd-15ad9fafc3bb" } } ``` ## 2. Create a payment intent This example asks the customer to fund a temporary TRON address, then sends the confirmed proceeds to an external address. > The path is currently case-sensitive: use `/payment/create-crypto-Payment-Intent` exactly as shown. ```bash curl --request POST \ --url https://bridge.relayfinance.io/payment/create-crypto-Payment-Intent \ --header 'Content-Type: application/json' \ --header 'accesskey: YOUR_ORGANIZATION_API_KEY' \ --data '{ "amount": 125.50, "txRef": "order-1042", "type": "crypto", "direction": "deposit", "cryptonetwork": "tron", "cryptotoken": "USDT", "metadata": { "orderId": "1042" }, "useremail": "buyer@example.com", "postTransactionType": "external_wallet", "postTransactionAddress": "TQ9ExampleDestinationAddress" }' ``` Relay returns HTTP `201 Created`. Use `data.tempWallet[0].address` as the deposit address and `data.expectedAmount` as the exact amount the customer must send. ```json { "success": true, "data": { "txRef": "order-1042", "type": "crypto", "direction": "deposit", "cryptonetwork": "tron", "cryptotoken": "USDT", "expectedAmount": "126.75", "originalAmount": "126.75", "confirmedAmount": "0", "outstandingAmount": "126.75", "status": "pending", "txId": "RLY-01J9Y7Y9F4M6", "tempWallet": [ { "address": "TX1ExampleDepositAddress", "network": "tron", "id": "wallet_example" } ] }, "meta": { "requestId": "72328264-aa68-4c0d-8553-811a1d25ca62" } } ``` The quoted `expectedAmount` can be greater than the requested `amount` because it includes the fee calculated for the organization and asset. Treat Relay's returned decimal string as authoritative. ## 3. Retrieve the intent Store `txId`, not the temporary wallet address, as your Relay resource identifier. ```javascript const response = await fetch( "https://bridge.relayfinance.io/payment/get-payment-intent/RLY-01J9Y7Y9F4M6", { headers: { accesskey: process.env.RELAY_ACCESS_KEY }, }, ); const body = await response.json(); if (!response.ok) throw new Error(`${body.error.code}: ${body.error.message}`); console.log(body.data.status, body.data.confirmedAmount); ``` The retrieval response uses the same payment object as creation. A successful HTTP response only means the lookup succeeded; inspect `data.status`, `data.confirmedAmount`, and `data.settlementStatus` for the business state. ## 4. Add webhooks Configure a public HTTPS endpoint in the Relay dashboard. Verify `Relay-Signature` against the exact raw request body, acknowledge with any `2xx` response, and deduplicate by `Relay-Event-Id`. Continue with [webhook verification](/docs/webhooks) before using events in production. --- ## Authentication Source: /docs/getting-started/authentication # Authentication Relay customer API requests authenticate with an organization API key in the `accesskey` header. ```http accesskey: YOUR_ORGANIZATION_API_KEY ``` ## Server-side only An organization API key can create collection resources and read data belonging to its organization. Treat it like a password: - call Relay from your backend, never directly from a browser or mobile client; - store it in a secret manager or protected environment variable; - do not include it in source control, analytics, URLs, screenshots, or application logs; and - rotate it through your Relay onboarding contact if you believe it has been exposed. ```javascript const relay = async (path, init = {}) => { const response = await fetch(`https://bridge.relayfinance.io${path}`, { ...init, headers: { "Content-Type": "application/json", accesskey: process.env.RELAY_ACCESS_KEY, ...init.headers, }, }); const body = await response.json(); if (!response.ok) { const error = new Error(body.error?.message ?? "Relay request failed"); error.code = body.error?.code; error.requestId = body.meta?.requestId; throw error; } return body.data; }; ``` ## Authentication failures | Condition | HTTP status | Message | | --- | ---: | --- | | Header is missing | `401` | `Provide Access Key` | | Key is invalid or the organization is inactive | `401` | `Invalid Access Key` | | Authenticated organization does not own the resource | `404` or `400` | Resource-specific safe message | Authentication errors use the standard error envelope: ```json { "success": false, "error": { "code": "UNAUTHORIZED", "message": "Invalid Access Key" }, "meta": { "requestId": "e4d95367-72b7-4638-ada4-1715ec98f37c" } } ``` ## Tenant boundaries Every API-key request is scoped to the organization that owns the key. Relay does not use an organization ID supplied by the caller to select a tenant. A payment or wallet belonging to another organization is not returned. ## Dashboard sessions are different The Relay dashboard uses bearer-token sessions and role permissions for human administration. Those endpoints manage settings such as settlement destinations, webhook endpoints, team access, and delivery replay. They are not part of the server API documented here, and an `accesskey` cannot be used as a dashboard bearer token. ## Request IDs Relay generates a UUID for each HTTP request and returns it in both places below: ```http X-Request-Id: 923d2eb7-361e-4a56-a846-e5fc81065864 ``` ```json { "meta": { "requestId": "923d2eb7-361e-4a56-a846-e5fc81065864" } } ``` Log this value with your internal request or order ID. Include it when contacting Relay about a failed or unexpected request. You may send your own `X-Request-Id`, but Relay currently generates the authoritative response request ID. --- ## Errors and retries Source: /docs/getting-started/errors # Errors and retries Relay returns stable JSON envelopes for application errors. Use the HTTP status for transport behavior, `error.code` for program logic, and `meta.requestId` for investigation. ```json { "success": false, "error": { "code": "VALIDATION_FAILED", "message": "One or more fields are invalid.", "details": [ { "field": "useremail", "code": "isEmail" } ] }, "meta": { "requestId": "ab16ed04-fceb-4415-a609-967bc77972fa" } } ``` ## HTTP status codes | Status | Default code | Meaning | What to do | | ---: | --- | --- | --- | | `400` | `INVALID_REQUEST` or `VALIDATION_FAILED` | The body, query, asset, amount, or workflow rule is invalid | Correct the request; do not retry unchanged | | `401` | `UNAUTHORIZED` | The `accesskey` is missing, invalid, or inactive | Fix or rotate credentials | | `403` | `FORBIDDEN` | The authenticated actor is not allowed to perform the operation | Review account access; do not retry unchanged | | `404` | `NOT_FOUND` or a resource code | The tenant-scoped resource does not exist | Check the resource identifier | | `409` | `CONFLICT` | The request conflicts with current resource state | Retrieve current state before deciding whether to retry | | `422` | `VALIDATION_FAILED` | Semantically invalid input on endpoints that use this status | Correct the fields shown in `details` | | `429` | `RATE_LIMITED` | A plan or protection limit was reached | Respect `Retry-After` when present and back off | | `500` | `INTERNAL_ERROR` | Relay encountered an unexpected error | Retry with backoff; contact Relay if it persists | Relay does not publish one global request quota. Limits may vary by environment or commercial plan. Build clients that can safely handle `429` without assuming a fixed requests-per-minute value. ## Business validation errors Some `400` responses have a message specific to the business rule. Common examples include: | Message or code | Cause | | --- | --- | | `Invalid Crypto Token` | The network/token pair is not active for payment intents | | `INVALID_PAYMENT_AMOUNT` | The amount is non-positive or cannot fit the token precision | | `External Wallet Address is Required` | `external_wallet` was selected without a destination | | `Asset is unavailable` | A customer-wallet asset is inactive or missing its contract configuration | | `Customer wallet fees are not configured for this asset` | Relay has not configured a fee policy for the organization and asset | | `PAYMENT_NOT_FOUND` | No payment intent with that `txId` belongs to the organization | Messages add context but may become more specific. Prefer branching on `error.code` where a dedicated code exists. ## Retry policy Retry only errors that can reasonably recover without changing the request: - network failures before you receive a response; - `429` responses, honoring a bounded `Retry-After`; and - `500` responses with exponential backoff and jitter. Do not automatically retry `400`, `401`, `403`, `404`, or ordinary `409` responses. ```javascript const retryable = response.status === 429 || response.status >= 500; const retryAfter = Number(response.headers.get("retry-after") ?? 0) * 1000; const delay = Math.max(retryAfter, Math.min(30_000, 500 * 2 ** attempt)); ``` ## Creation and idempotency `txRef` is your merchant reference; it is not an HTTP idempotency key. The current payment-intent creation routes do not accept a documented idempotency header. A repeated creation request can create another intent. For ambiguous timeouts: 1. record whether Relay returned a `txId` before retrying; 2. use a durable local state machine around each order; 3. avoid blindly sending the same create request again; and 4. reconcile with your stored Relay resource or contact Relay using the request ID when the outcome is uncertain. Read endpoints are safe to retry. Webhook deliveries are explicitly at-least-once and keep the same event ID across retries. --- ## Crypto payment intents Source: /docs/payments/crypto-payment-intents # Crypto payment intents A crypto payment intent creates a temporary collection address for one order or invoice. Relay watches the address, accounts for confirmed funds, and manages the configured post-payment settlement flow. ## Create a crypto payment intent `POST /payment/create-crypto-Payment-Intent` Returns HTTP `201 Created` with a [payment object](/docs/reference/payment-object). ### Request fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `amount` | number | yes | Merchant amount before Relay's quoted crypto fee. Must be positive and fit token precision. | | `txRef` | string | yes | Your order or invoice reference. This is not an idempotency key. | | `type` | string | yes | Must be `crypto`. | | `direction` | string | yes | Use `deposit` for the supported collection flow. | | `cryptonetwork` | string | yes | Enabled network returned by the active-assets endpoint; currently `tron` or `solana`. | | `cryptotoken` | string | yes | Enabled token for the selected network, normally `USDT` or `USDC`. | | `metadata` | object | yes | Your structured data. Keep it small and do not place secrets in it. | | `useremail` | string | yes | Valid customer email address. | | `postTransactionType` | string | yes | `external_wallet`, `organization_wallet`, or `organization_settlement`. Solana supports the first and third options. | | `postTransactionAddress` | string | conditional | Required only for `external_wallet`; omit for the other modes. Maximum 150 characters. | ### Settlement modes | Value | Destination behavior | | --- | --- | | `external_wallet` | You supply `postTransactionAddress` in the request. | | `organization_wallet` | Relay resolves the organization's Relay-managed wallet for the network. Not supported for Solana payment intents. | | `organization_settlement` | Relay resolves the saved network/token settlement destination configured in the dashboard. Incoming payments only. | ```json { "amount": 250, "txRef": "invoice-8091", "type": "crypto", "direction": "deposit", "cryptonetwork": "solana", "cryptotoken": "USDC", "metadata": { "invoiceId": "8091", "customerId": "cus_7a2" }, "useremail": "finance@example.com", "postTransactionType": "organization_settlement" } ``` ```python import os import requests response = requests.post( "https://bridge.relayfinance.io/payment/create-crypto-Payment-Intent", headers={"accesskey": os.environ["RELAY_ACCESS_KEY"]}, json={ "amount": 250, "txRef": "invoice-8091", "type": "crypto", "direction": "deposit", "cryptonetwork": "solana", "cryptotoken": "USDC", "metadata": {"invoiceId": "8091", "customerId": "cus_7a2"}, "useremail": "finance@example.com", "postTransactionType": "organization_settlement", }, timeout=20, ) response.raise_for_status() payment = response.json()["data"] ``` ### Deposit instructions For TRON, send `expectedAmount` of `cryptotoken` to `tempWallet[0].address` before `paymentWindowEndsAt` when that field is present. For Solana SPL tokens, the response also provides: - `chainIdentity`: the pinned Solana chain identity; - `tokenContractAddress`: the token mint; - `tokenStandard`: `SPL`; - `tokenDecimals`: `6`; and - `depositTokenAccount`: the associated token account that should receive the SPL transfer. Intent creation derives the Solana token account but does not create it on-chain. A custom checkout should include idempotent associated-token-account creation when necessary, and should verify that the sending wallet or exchange supports that destination. ## Retrieve a payment intent `GET /payment/get-payment-intent/{txId}` | Path field | Type | Description | | --- | --- | --- | | `txId` | string | Relay transaction identifier returned when the intent was created. | ```bash curl --request GET \ --url https://bridge.relayfinance.io/payment/get-payment-intent/RLY-01J9Y7Y9F4M6 \ --header 'accesskey: YOUR_ORGANIZATION_API_KEY' ``` A tenant-scoped miss returns HTTP `404` with code `PAYMENT_NOT_FOUND`. Retrieval does not change the payment or renew its collection window. ## List active assets `GET /payment/get-active-payment-intents-currency` Returns the payment-intent assets that are active in the current Relay deployment. Each item has: | Field | Type | Description | | --- | --- | --- | | `name` | string | Display name configured by Relay. | | `network` | string | Network identifier. | | `token` | string | Asset symbol. | Asset availability is operational configuration, not a permanent guarantee. Query this endpoint instead of treating the enums in this documentation as an allow-list. ## Funding and settlement Incoming accounting and outgoing settlement are independent: - `confirmedAmount` is the eligible confirmed total received; - `outstandingAmount` is the remaining target; - `status: successful` means the payment has been funded; - `settlementStatus` describes the outgoing settlement separately; and - `status: completed` is a later lifecycle state after Relay's completion workflow. Use payment webhooks for timely transitions and retrieval for reconciliation. Never mark an order paid based only on HTTP `200` from the lookup endpoint. ## Common errors | HTTP | Message or code | Cause | | ---: | --- | --- | | `400` | `Invalid Amount` or `INVALID_PAYMENT_AMOUNT` | Non-positive amount or invalid token precision | | `400` | `Invalid Crypto Token` | Inactive or unsupported network/token pair | | `400` | `External Wallet Address is Required` | Missing destination in external-wallet mode | | `400` | `Invalid Post Transaction Type` | A destination was supplied for a non-external mode | | `400` | `Organization wallet not found…` | No managed organization wallet exists for the network | | `400` | `Solana payment configuration or destination is invalid or unavailable` | Chain, mint, or destination validation failed | | `404` | `PAYMENT_NOT_FOUND` | The `txId` is absent or belongs to another organization | --- ## Fiat payment intents Source: /docs/payments/fiat-payment-intents # Fiat payment intents A fiat payment intent creates a time-limited virtual bank account for an NGN collection and records the approved crypto settlement destination. > Fiat availability depends on the banking provider and settlement configuration enabled for your organization. The current API accepts NGN. With the currently enabled network policy, the production path is TRON settlement using an active supported token such as USDC. Confirm the exact asset during onboarding. ## Get the current fiat rate `GET /payment/fiat-rate?currency=ngn` | Query field | Type | Required | Description | | --- | --- | --- | --- | | `currency` | string | no | Defaults to `ngn`. Other currency values currently return an error. | ```bash curl --request GET \ --url 'https://bridge.relayfinance.io/payment/fiat-rate?currency=ngn' \ --header 'accesskey: YOUR_ORGANIZATION_API_KEY' ``` ```json { "success": true, "data": { "currency": "ngn", "rate": 1652.5 }, "meta": { "requestId": "ee26a15c-6224-4d2e-9c78-5574a80c358c" } } ``` `rate` is the buffered NGN-per-USD conversion rate as a JSON number. It is indicative and can change before intent creation; use the values returned on the created payment as the final quote for that intent. ## Create a fiat payment intent `POST /payment/create-fiat-payment-intent` Returns HTTP `201 Created` with a payment object and `fiatDetail` virtual-account instructions. ### Request fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `amount` | number | yes | Fiat amount the customer must pay. Must be greater than zero. | | `currency` | string | yes | Currently `ngn`. | | `txRef` | string | yes | Your merchant order or invoice reference. Not an idempotency key. | | `useremail` | string | yes | Valid customer email address. | | `postTransactionType` | string | no | `external_wallet` by default, or `organization_settlement`. | | `settlement` | object | yes | Crypto network, token, and—when required—destination. | | `settlement.network` | string | yes | Enabled settlement network. | | `settlement.token` | string | yes | Enabled settlement token. Fiat logic permits USDC and, where the network is enabled, ETH. | | `settlement.address` | string | conditional | Required for `external_wallet`; must be omitted for `organization_settlement`. | | `validFor` | number | no | Requested virtual-account validity in seconds. The provider may return the effective value. | | `metadata` | object | no | Your structured correlation data. | ```json { "amount": 500000, "currency": "ngn", "txRef": "invoice-2218", "useremail": "buyer@example.com", "postTransactionType": "organization_settlement", "settlement": { "network": "tron", "token": "USDC" }, "validFor": 1800, "metadata": { "customerId": "cus_18fd" } } ``` ### Virtual-account response ```json { "success": true, "data": { "txRef": "invoice-2218", "type": "fiat", "direction": "deposit", "cryptonetwork": "tron", "cryptotoken": "USDC", "expectedAmount": "302.571860817", "rate": "1652.5", "expectedSettlementTokenAmount": "302.571860817", "virtualAccountExpiresAt": "2026-09-20T12:30:00.000Z", "virtualAccountExpiresIn": 1800, "txId": "RLY-01K5W6K8A2T9", "status": "pending", "fiatDetail": { "fiatCurrency": "ngn", "fiatAmount": "500000", "settlementNetwork": "tron", "settlementToken": "USDC", "settlementAddress": "TQ9ExampleDestinationAddress", "virtualAccountNumber": "1234567890", "virtualAccountBankCode": "090286", "virtualAccountAccountName": "Relay / invoice-2218", "virtualAccountCurrencyCode": "NGN", "virtualAccountStatus": "ACTIVE", "virtualAccountValidFor": 1800, "virtualAccountExpiryDate": "2026-09-20T12:30:00.000Z" } }, "meta": { "requestId": "1563bb6a-bc85-4d33-b2c3-f5d42af9120e" } } ``` Display the account name, number, bank details, exact NGN amount, and expiry to the customer. Do not display or use the internal callback URL if one appears in `fiatDetail`. ## Retrieve and reconcile Use the same retrieval route as crypto payments: `GET /payment/get-payment-intent/{txId}` The response contains the same payment and fiat fields. Bank-provider notifications update the intent asynchronously, so receive [payment webhooks](/docs/webhooks) and periodically reconcile pending records through retrieval. ## Common errors | HTTP | Message | Cause | | ---: | --- | --- | | `400` | `Only NGN is supported for fiat intents` | Unsupported fiat currency | | `400` | `Invalid settlement details` | Missing network, token, or required address | | `400` | `Omit settlement.address when using organization_settlement` | Address supplied when Relay should resolve the saved destination | | `400` | `Network is disabled` | Settlement network is not enabled for new work | | `400` | `Solana is supported for crypto payment intents only` | Solana selected for fiat settlement | | `400` | `Only USDC and ETH are supported for settlement` | Unsupported settlement asset | | `400` | `Unable to fetch exchange rate` | The pricing provider is temporarily unavailable | | `400` | provider/configuration-specific message | Virtual-account creation or settlement configuration failed | --- ## Customer deposit wallets Source: /docs/wallets/customer-wallets # Customer deposit wallets Customer wallets give a known customer a reusable deposit address. Relay associates confirmed deposits with your `userId`, calculates the configured fee, and queues the merchant amount for settlement to the saved organization destination. Use this model for account funding and repeated deposits. Use a [payment intent](/docs/payments/crypto-payment-intents) when each order needs its own amount and lifecycle. ## Get or create a wallet `POST /customer-wallets` The operation is get-or-create for the organization, case-sensitive `userId`, network, and pinned chain identity. Calling it again returns the existing address, enrolls the requested token if needed, refreshes its saved settlement destination, and renews a two-hour fast-detection window. ### Request fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `userId` | string | yes | Your stable, organization-scoped customer ID. Case-sensitive; 1–128 characters. | | `network` | string | yes | `tron` or `solana`. | | `token` | string | yes | `USDT` or `USDC`, subject to active configuration. | ```bash curl --request POST \ --url https://bridge.relayfinance.io/customer-wallets \ --header 'Content-Type: application/json' \ --header 'accesskey: YOUR_ORGANIZATION_API_KEY' \ --data '{ "userId": "customer_28491", "network": "solana", "token": "USDC" }' ``` ```json { "success": true, "data": { "id": "cwallet_example", "assetId": "cwasset_example", "userId": "customer_28491", "network": "solana", "chainIdentity": "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "address": "HkYExampleOwnerAddress", "token": "USDC", "tokenContract": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", "tokenAccount": "3pExampleAssociatedTokenAccount", "watchUntil": "2026-09-20T14:00:00.000Z", "retainedFees": "0.000000", "nextFeeSweepAt": "2026-10-01T00:00:00.000Z" }, "meta": { "requestId": "7944aed7-cef2-4d8f-b942-3d8e4974f57f" } } ``` ### Response fields | Field | Type | Description | | --- | --- | --- | | `id` | string | Wallet ID used by the read and history endpoints. | | `assetId` | string | Enrollment ID for this wallet/token pair. | | `userId` | string | Exact customer ID supplied by your system. | | `network` | string | Wallet network. | | `chainIdentity` | string | Pinned chain identity used for evidence validation. | | `address` | string | Owner/deposit address. | | `token` | string | Enrolled token. | | `tokenContract` | string | Pinned token contract or mint. | | `tokenAccount` | string or null | Solana associated token account; `null` on TRON. | | `watchUntil` | string | End of the renewed fast-detection window. | | `retainedFees` | decimal string | Confirmed, unreserved Relay fees waiting for the scheduled fee sweep. | | `nextFeeSweepAt` | string | Next scheduled fee collection date. | Wallet creation requires an active asset, token contract, configured customer-wallet fee policy, and saved settlement destination for the organization. The saved destination cannot be the customer deposit address. ## Get a wallet `GET /customer-wallets/{id}` Returns the wallet and every token enrollment currently attached to it. ```json { "success": true, "data": { "id": "cwallet_example", "userId": "customer_28491", "network": "solana", "address": "HkYExampleOwnerAddress", "assets": [ { "assetId": "cwasset_example", "token": "USDC", "tokenAccount": "3pExampleAssociatedTokenAccount", "retainedFees": "0.000000" } ] }, "meta": { "requestId": "946b12aa-7b1f-48b0-8653-ed71936617f6" } } ``` ## List deposits `GET /customer-wallets/{id}/deposits?limit=20&offset=0` | Query field | Type | Default | Limits | | --- | --- | ---: | --- | | `limit` | integer | `20` | 1–100 | | `offset` | integer | `0` | 0–1,000,000 | The endpoint returns an array in `data` and pagination details in `meta.pagination`. ```json { "success": true, "data": [ { "id": "deposit_example", "assetId": "cwasset_example", "transactionHash": "5TfExampleSignature", "blockTimestamp": "2026-09-20T11:51:04.000Z", "amount": "100.000000", "fee": "1.000000", "merchantAmount": "99.000000", "status": "confirmed", "createdAt": "2026-09-20T11:51:10.000Z", "updatedAt": "2026-09-20T11:51:10.000Z" } ], "meta": { "requestId": "57738670-36ad-46ed-8fb2-624ca1d39183", "pagination": { "total": 1, "limit": 20, "offset": 0, "hasMore": false } } } ``` Deposit status starts as `confirmed` when it is eligible for settlement, or `review_required` when Relay cannot safely apply a fee and payout. After the linked merchant transfer is verified, the deposit becomes `settled`; an uncertain transfer can also move it to `review_required`. ## List transfers `GET /customer-wallets/{id}/transfers?limit=20&offset=0` Transfer records describe outgoing merchant payouts and periodic Relay fee collections. | Field | Type | Description | | --- | --- | --- | | `kind` | string | `merchant` or `fee`. | | `depositId` | string or null | Source deposit for a merchant transfer. | | `destination` | string | Destination captured when the transfer was queued. | | `amount` | decimal string | Exact outgoing amount. | | `status` | string | `queued`, `submitted`, `settled`, or `review_required`. | | `transactionHash` | string or null | On-chain hash after submission. | | `lastError` | string or null | Safe operational status for deferred or reviewed work. | | `nextAttemptAt` | string | Next scheduled processing or verification time. | One outgoing operation runs at a time per wallet across all enrolled tokens. A `review_required` transfer blocks later outgoing work until Relay reconciles it; do not attempt to replace or resend the transfer yourself. ## Webhook events Subscribe to the `wallet_event` action in the dashboard. Customer-wallet event types are: - `customer_wallet.deposit_confirmed` - `customer_wallet.deposit_review_required` - `customer_wallet.merchant_settled` - `customer_wallet.merchant_review_required` - `customer_wallet.fee_settled` - `customer_wallet.fee_review_required` - `customer_wallet.transfer_review_required` `deposit_confirmed` means the incoming funds were credited and a payout was queued. Only `merchant_settled` proves the outgoing merchant payout was verified on-chain. ## Common errors | HTTP | Message | Cause | | ---: | --- | --- | | `400` | `Asset is unavailable` | Inactive asset, missing contract, or unsupported configuration | | `400` | `Customer wallet fees are not configured for this asset` | No applicable fee policy | | `400` | saved settlement error | No approved settlement destination for the network/token | | `400` | `Wallet asset contract changed; reconciliation required` | Existing wallet enrollment is pinned to another contract | | `404` | `Customer wallet not found` | The ID is absent or belongs to another organization | --- ## External wallet monitoring Source: /docs/wallets/external-wallets # External wallet monitoring Register an address your organization already owns when Relay should track token deposits or withdrawals and emit signed wallet events. This API does not create the wallet or expose its private key. External wallet monitoring is different from [customer wallets](/docs/wallets/customer-wallets): it observes an existing organization address and does not run the customer-wallet fee and automatic per-customer settlement workflow. ## Check whether a wallet exists `GET /wallet/organization/external-wallet?address={address}&network={network}` | Query field | Type | Required | Description | | --- | --- | --- | --- | | `address` | string | yes | Exact wallet address. | | `network` | string | yes | Enabled network. New registrations currently accept `tron` or `solana`. | ```bash curl --get \ --url https://bridge.relayfinance.io/wallet/organization/external-wallet \ --header 'accesskey: YOUR_ORGANIZATION_API_KEY' \ --data-urlencode 'address=TQ9ExampleAddress' \ --data-urlencode 'network=tron' ``` ```json { "success": true, "data": { "exists": true, "wallet": { "id": "wallet_example", "address": "TQ9ExampleAddress", "network": "tron", "source": "external", "eventSubscriptions": [] } }, "meta": { "requestId": "627cd68a-c806-4ed8-9a83-fb1a68dfec5d" } } ``` When no matching wallet belongs to the organization, `exists` is `false` and `wallet` is `null`. ## Register a wallet `POST /wallet/organization` Returns HTTP `200 OK` with the saved wallet and its event subscriptions. | Field | Type | Required | Description | | --- | --- | --- | --- | | `address` | string | yes | Existing organization-controlled address. | | `network` | string | yes | `tron` or `solana` for new work. | | `events` | array | no | Deposit or withdrawal subscriptions. An empty list registers without notifications. | | `events[].eventType` | string | yes | `deposit` or `withdrawal`. | | `events[].network` | string | yes | Must match the wallet network. | | `events[].token` | string | yes | Token symbol configured on the selected network. | | `events[].requiredBlockConfirmation` | number | no | Non-negative confirmation threshold. Relay applies its configured default when omitted. | ```json { "address": "TQ9ExampleAddress", "network": "tron", "events": [ { "eventType": "deposit", "network": "tron", "token": "USDT", "requiredBlockConfirmation": 20 }, { "eventType": "withdrawal", "network": "tron", "token": "USDT", "requiredBlockConfirmation": 20 } ] } ``` Each direction/network/token tuple must be unique. The service rejects an event whose network differs from the wallet network. ## Replace event subscriptions `PUT /wallet/organization/{walletId}/events` This operation replaces the complete subscription set for the wallet. Send every subscription that should remain active. ```json { "events": [ { "eventType": "deposit", "network": "tron", "token": "USDC" } ] } ``` The returned `data.events` array contains the persisted subscription records and effective `requiredBlockConfirmation` values. ## Wallet webhook payload Subscribed confirmed activity produces `wallet.deposit` or `wallet.withdrawal` events under the `wallet_event` action. ```json { "id": "event_example", "type": "wallet.deposit", "schemaVersion": "1", "occurredAt": "2026-09-20T12:05:20.000Z", "resourceVersion": 4, "data": { "wallet": { "id": "wallet_example", "address": "TQ9ExampleAddress", "network": "tron" }, "transaction": { "id": "transaction_example", "amount": "85.250000", "token": "USDT", "network": "tron", "transactionHash": "fe3ExampleHash", "blockConfirmations": 20, "requiredBlockConfirmation": 20, "direction": "deposit" } } } ``` ## Common errors | HTTP | Message | Cause | | ---: | --- | --- | | `400` | `Address and network are required` | Missing lookup query fields | | `400` | `Wallet Already Exist` | The address/network is already registered globally | | `400` | `Duplicate wallet event subscription` | Repeated direction/network/token tuple | | `400` | `Event network … does not match wallet network …` | Subscription uses another network | | `400` | `Invalid Wallet` | Wallet ID is absent or not owned by the organization | --- ## Webhooks Source: /docs/webhooks # Webhooks Relay sends signed HTTPS requests when payment or wallet state changes. Delivery is at-least-once: the same event can arrive more than once, and different events for one resource can arrive out of order. Use webhooks for prompt updates and a GET endpoint for reconciliation. ## Configure an endpoint Webhook endpoints are an administrative control in the Relay dashboard, not an `accesskey` endpoint. A team member with webhook administration permission can add an endpoint and choose one or more action groups: | Action | Delivers | | --- | --- | | `payment_intent` | Crypto payment-intent lifecycle events | | `fiat_payment_intent` | Fiat payment-intent lifecycle events | | `wallet_event` | External-wallet and customer-wallet events | Your endpoint must: - use public HTTPS on port 443; - resolve only to public IP addresses; - not contain embedded credentials or a URL fragment; and - accept requests without relying on redirects, because Relay does not follow them. Relay reveals the endpoint signing secret when the endpoint is created. Store the `whsec_…` value immediately; normal endpoint reads do not expose it. ## Delivery request ```http POST /relay/webhooks HTTP/1.1 Content-Type: application/json Relay-Event-Id: 56bb88e4-682a-4211-9847-144becfb5266 Relay-Signature: t=1789905920,v1=cb24b9… ``` The signature header can temporarily contain two `v1` values during secret rotation. Accept the request when any valid `v1` signature matches an active secret. ## Event envelope ```json { "id": "56bb88e4-682a-4211-9847-144becfb5266", "type": "payment_intent.funded", "schemaVersion": "1", "occurredAt": "2026-09-20T12:05:20.000Z", "resourceVersion": 3, "data": { "txId": "PMI-example", "status": "successful", "confirmedAmount": "126.750000", "outstandingAmount": "0" } } ``` | Field | Type | Description | | --- | --- | --- | | `id` | string | Immutable business-event ID. It stays the same across delivery retries and replays. | | `type` | string | Business transition name. | | `schemaVersion` | string | Event schema version, currently `1`. | | `occurredAt` | string | Time the committed business event was created. | | `resourceVersion` | integer | Monotonic version for that resource. A lower version must not roll your state backward. | | `data` | object | Allow-listed payment, wallet, deposit, or transfer snapshot. | The `Relay-Event-Id` header equals the JSON body's `id`. ## Verify signatures Relay computes an HMAC-SHA256 digest over: ```text . ``` The digest is lowercase hexadecimal. Verify the raw bytes before parsing JSON, use constant-time comparison, and reject timestamps outside a five-minute tolerance. ```javascript import crypto from "node:crypto"; export function verifyRelayWebhook(rawBody, signatureHeader, secret) { const parts = signatureHeader.split(",").map((part) => part.trim()); const timestampParts = parts.filter((part) => part.startsWith("t=")); const signatures = parts .filter((part) => part.startsWith("v1=")) .map((part) => part.slice(3)); if (timestampParts.length !== 1 || signatures.length === 0) return false; const timestamp = timestampParts[0].slice(2); if (!/^\d+$/.test(timestamp)) return false; const age = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp)); if (age > 300) return false; const expected = crypto .createHmac("sha256", secret) .update(`${timestamp}.${rawBody}`, "utf8") .digest("hex"); return signatures.some((candidate) => { if (!/^[a-f0-9]{64}$/.test(candidate)) return false; return crypto.timingSafeEqual( Buffer.from(expected, "hex"), Buffer.from(candidate, "hex"), ); }); } ``` In Express, register a raw-body parser for the webhook route. In frameworks that parse JSON automatically, use the framework's documented raw-body hook. Re-stringifying parsed JSON changes whitespace or key representation and invalidates the signature. ## Process safely Your handler should do only the work needed to authenticate and durably enqueue the event, then return a `2xx` response. ```javascript app.post("/relay/webhooks", rawBodyMiddleware, async (request, response) => { const rawBody = request.body.toString("utf8"); const valid = verifyRelayWebhook( rawBody, request.header("Relay-Signature") ?? "", process.env.RELAY_WEBHOOK_SECRET, ); if (!valid) return response.status(400).send("invalid signature"); const event = JSON.parse(rawBody); await events.insertIfAbsent(event.id, rawBody); // unique event.id response.status(204).end(); }); ``` When processing asynchronously: 1. deduplicate on `event.id`; 2. keep the original event body for audit; 3. compare `resourceVersion` before updating a local projection; 4. treat incoming funding and outgoing settlement as separate facts; and 5. retrieve the resource if an event cannot be applied confidently. ## Event catalog ### Payment events | Event type | Meaning | | --- | --- | | `payment_intent.partially_funded` | Confirmed eligible funds are below the target. | | `payment_intent.funded` | The required incoming amount is confirmed. Settlement can still be pending. | | `payment_intent.pending_verification` | Relay has evidence that still requires verification. | | `payment_intent.awaiting_confirmation` | A detected transaction has not reached required confirmation. | | `payment_intent.settlement_submitted` | Outgoing settlement was submitted; this does not prove finality. | | `payment_intent.settled` | Outgoing settlement was verified on-chain. | | `payment_intent.settlement_failed` | Outgoing settlement needs failure handling; incoming funding remains accounted for. | | `payment_intent.settlement_review_required` | The outgoing broadcast or accounting outcome requires reconciliation before another send. | | `payment_intent.completed` | The completion workflow reached its completed state. | | `payment_intent.failed` | Payment processing failed. | | `payment_intent.cancelled` | The payment was cancelled. | Payment `data` uses the same allow-listed schema as the [payment object](/docs/reference/payment-object). ### External wallet events - `wallet.deposit` - `wallet.withdrawal` The payload includes `data.wallet` and `data.transaction`, including the exact decimal amount, token, network, transaction hash, direction, and confirmation counts. ### Customer wallet events - `customer_wallet.deposit_confirmed` - `customer_wallet.deposit_review_required` - `customer_wallet.merchant_settled` - `customer_wallet.merchant_review_required` - `customer_wallet.fee_settled` - `customer_wallet.fee_review_required` - `customer_wallet.transfer_review_required` ## Acknowledgments and retries Any HTTP status from `200` through `299` acknowledges the delivery. Relay uses a 10-second request timeout by default. Relay retries: - network and timeout failures; - HTTP `408`; - HTTP `429`, honoring a bounded `Retry-After` value; and - HTTP `5xx`. Other `4xx` responses are terminal. The default maximum is 10 attempts, using exponential backoff with jitter from approximately 30 seconds up to one hour. Delivery records are retained for 30 days by default. These operational values can be changed by Relay configuration, so do not build timing-sensitive logic around them. A crash after your endpoint accepted an event but before Relay saved the acknowledgment can cause a retry. This is why deduplication is required even when your endpoint is reliable. ## Rotation, replay, and delivery controls Dashboard users with the relevant permissions can disable endpoints, rotate a signing secret, inspect deliveries and attempts, and replay a retained delivery. Rotation has an overlap period in which Relay signs with both the current and previous secret. A replay preserves the original event ID and body while creating a new auditable delivery attempt. --- ## API reference Source: /docs/reference/api-reference # API reference This page indexes every customer-facing server endpoint that authenticates with your organization `accesskey`. Dashboard-session and Relay-operator endpoints are intentionally excluded. Base URL: `https://bridge.relayfinance.io` ## Payments | Method | Path | Success | Purpose | | --- | --- | ---: | --- | | `POST` | `/payment/create-crypto-Payment-Intent` | `201` | Create a one-off crypto collection intent and temporary deposit wallet. | | `POST` | `/payment/create-fiat-payment-intent` | `201` | Create an NGN virtual-account collection intent. | | `GET` | `/payment/get-payment-intent/{txId}` | `200` | Retrieve a tenant-scoped crypto or fiat intent. | | `GET` | `/payment/get-active-payment-intents-currency` | `200` | List assets currently active for payment intents. | | `GET` | `/payment/fiat-rate?currency=ngn` | `200` | Get the current buffered NGN-per-USD rate. | Detailed inputs and responses: - [Crypto payment intents](/docs/payments/crypto-payment-intents) - [Fiat payment intents](/docs/payments/fiat-payment-intents) - [Payment object](/docs/reference/payment-object) ## Customer deposit wallets | Method | Path | Success | Purpose | | --- | --- | ---: | --- | | `POST` | `/customer-wallets` | `201` | Get or create a reusable wallet/token enrollment and renew its watch. | | `GET` | `/customer-wallets/{id}` | `200` | Retrieve a wallet and its enrolled assets. | | `GET` | `/customer-wallets/{id}/deposits` | `200` | List confirmed or review-required incoming deposits. | | `GET` | `/customer-wallets/{id}/transfers` | `200` | List merchant payouts and fee sweeps. | History query fields are `limit` (default 20, maximum 100) and `offset` (default 0). See [customer wallets](/docs/wallets/customer-wallets). ## External wallet monitoring | Method | Path | Success | Purpose | | --- | --- | ---: | --- | | `GET` | `/wallet/organization/external-wallet` | `200` | Find a registered external wallet by `address` and `network`. | | `POST` | `/wallet/organization` | `200` | Register an existing address and optional event subscriptions. | | `PUT` | `/wallet/organization/{walletId}/events` | `200` | Replace the wallet's complete event subscription set. | See [external wallets](/docs/wallets/external-wallets) for the nested event subscription fields. ## Authentication All endpoints on this page require: ```http accesskey: YOUR_ORGANIZATION_API_KEY ``` The key selects the organization. Do not send an organization ID in an attempt to change tenant scope. See [authentication](/docs/getting-started/authentication). ## Success envelope ```json { "success": true, "data": {}, "meta": { "requestId": "d01048f7-3874-4df5-8642-6ab07a66ea77" } } ``` `data` can be an object, array, primitive value, or `null`, depending on the endpoint. Paginated responses move the service's item list into `data` and add: ```json { "meta": { "requestId": "d01048f7-3874-4df5-8642-6ab07a66ea77", "pagination": { "total": 47, "limit": 20, "offset": 20, "hasMore": true } } } ``` ## Error envelope ```json { "success": false, "error": { "code": "INVALID_REQUEST", "message": "Asset is unavailable" }, "meta": { "requestId": "c23db470-9ac0-492b-8be9-26cfe667b577" } } ``` Validation errors can also include `error.details`, an array of `{ field, code }` objects. See [errors and retries](/docs/getting-started/errors). ## Compatibility rules - Treat undocumented response fields as non-contractual and ignore them safely. - Do not depend on object key order. - Preserve decimal strings as strings or arbitrary-precision decimals. - Accept new enum values without crashing; log and reconcile unknown states. - Use `schemaVersion` for webhook payload evolution. - Use the active-assets endpoint for availability rather than assuming every enum is enabled. ## Dashboard-only controls Human administration uses the Relay dashboard's bearer-token session and permissions. This includes: - settlement destination management; - webhook endpoint creation, editing, secret rotation, delivery inspection, and replay; - team membership and role controls; - payment lists, analytics, and receipt download; and - organization finance views. These controls are not authenticated with `accesskey` and are not part of the embedded-customer server API above. --- ## Payment object Source: /docs/reference/payment-object # Payment object Crypto creation, fiat creation, payment retrieval, and payment webhooks use the same allow-listed payment schema. Fields that do not apply to a payment type can be absent or `null`. ## Identity and lifecycle | Field | Type | Description | | --- | --- | --- | | `id` | string | Internal public resource ID. | | `txId` | string | Relay transaction identifier used by the retrieval endpoint. | | `txRef` | string | Merchant reference supplied at creation. Not an idempotency key. | | `type` | string | `crypto` or `fiat`. | | `direction` | string | Payment direction; supported collection requests use `deposit`. | | `status` | string | Overall payment lifecycle state. See [statuses](/docs/reference/statuses-and-assets). | | `resourceVersion` | integer | Monotonic public-resource version used by webhooks. | | `accountingState` | string | `ready` or `review_required` for confirmed-funding accounting. | | `createdAt` | string | Creation time in ISO 8601 UTC. | | `updatedAt` | string | Last update time in ISO 8601 UTC. | | `fundedAt` | string or null | Time Relay established that the target was fully funded. | | `paymentWindowEndsAt` | string or null | End of the active funding window when that network uses one. | | `paymentWindowClosedAt` | string or null | Time the funding window was closed. | ## Asset and chain | Field | Type | Description | | --- | --- | --- | | `cryptonetwork` | string | Funding or settlement network. | | `cryptotoken` | string | Funding or settlement asset symbol. | | `tokenContractAddress` | string or null | Token contract or mint pinned when the payment was created. | | `chainId` | integer or null | Numeric EVM chain ID where applicable; `null` for Solana. | | `chainIdentity` | string or null | Pinned non-EVM chain identity, including Solana genesis identity. | | `tokenStandard` | string or null | `SPL` for Solana payment intents. | | `tokenDecimals` | integer or null | Token precision Relay expects for the pinned asset. | | `depositTokenAccount` | string or null | Solana associated token account that receives the SPL transfer. | Use these pinned fields for the payment. Do not replace them with newer runtime asset configuration after an intent has been created. ## Amounts and accounting All fields in this section are exact decimal strings. | Field | Description | | --- | --- | | `expectedAmount` | Exact amount expected at the funding address. For crypto, this includes the quoted fee; for fiat, it represents expected settlement-token proceeds. | | `originalAmount` | Immutable confirmed-funding target for crypto accounting. | | `confirmedAmount` | Cumulative eligible amount confirmed on-chain. | | `outstandingAmount` | Remaining amount required to reach the target. | Do not use binary floating-point math for these values. Use an arbitrary-precision decimal library and compare values in the token's precision. ## Customer and settlement | Field | Type | Description | | --- | --- | --- | | `useremail` | string | Customer email supplied at creation. | | `metadata` | object | Merchant metadata supplied at creation. | | `postTransactionType` | string | `external_wallet`, `organization_wallet`, or `organization_settlement`. | | `postTransactionAddress` | string or null | Destination snapshot resolved for this payment. Later dashboard changes do not alter it. | | `settlementStatus` | string | Independent outgoing-settlement state. | | `settlementTransactionHash` | string or null | Outgoing settlement transaction hash once available. | Incoming funding never disappears because outgoing settlement failed. Reconcile `status`, `confirmedAmount`, and `settlementStatus` independently. ## Temporary wallets `tempWallet` is an array. Creation currently returns one temporary wallet. | Field | Type | Description | | --- | --- | --- | | `id` | string | Temporary wallet resource ID. | | `address` | string | Owner/deposit address. For Solana tokens, use `depositTokenAccount` as the SPL destination. | | `network` | string | Wallet network. | | `createdAt` | string | Creation time. | | `updatedAt` | string | Last update time. | ## Amount trackers `paymentamount` is an array of funding and offset accounting records. | Field | Type | Description | | --- | --- | --- | | `id` | string | Tracker ID. | | `trackerType` | string | `payment_intent` or `payment_intent_offset`. | | `currencyType` | string | Currency type associated with the tracker. | | `expectedAmount` | decimal string | Expected amount for this tracker. | | `amount` | decimal string or null | Observed/applied amount. | | `feeincrypto` | decimal string or null | Crypto fee associated with the quote. | | `feeinfiat` | decimal string or null | Fiat fee when applicable. | | `fiatcurrency` | string or null | Fiat currency when applicable. | | `tempWallet` | string or null | Associated temporary wallet ID. | | `transactionId` | string or null | Associated transaction identifier. | | `txHash` | string or null | On-chain transaction hash. | | `completedIntent` | boolean or null | Whether the record completed its intent contribution. | | `status` | string or null | Tracker status. | | `createdAt` | string | Creation time. | | `updatedAt` | string | Last update time. | ## Fiat summary fields These top-level convenience fields are present for fiat intents when available: | Field | Type | Description | | --- | --- | --- | | `rate` | decimal string | Buffered fiat-per-USD rate captured for the intent. | | `expectedSettlementTokenAmount` | decimal string | Expected crypto settlement amount. | | `virtualAccountExpiresAt` | string or null | Virtual-account expiry time. | | `virtualAccountExpiresIn` | integer or null | Provider validity duration in seconds. | ## Fiat detail `fiatDetail` contains the collection quote, virtual-account instructions, and settlement snapshot. | Field | Type | Description | | --- | --- | --- | | `fiatCurrency` | string | Fiat currency, currently `ngn`. | | `fiatAmount` | decimal string | Exact customer payment amount. | | `usdPerFiat` | decimal string | Source USD per fiat-unit rate. | | `fiatPerUsd` | decimal string | Inverse source rate before Relay's configured buffer. | | `bufferAmountNgn` | decimal string | Fixed NGN amount added to the effective fiat-per-USD rate. | | `bufferedFiatPerUsd` | decimal string | Effective quoted fiat-per-USD rate. | | `usdAmount` | decimal string | Converted USD value. | | `tokenUsdRate` | decimal string | Settlement token's USD rate. | | `expectedTokenAmount` | decimal string | Expected settlement-token amount. | | `settlementNetwork` | string | Destination network snapshot. | | `settlementToken` | string | Destination token snapshot. | | `settlementAddress` | string | Destination address snapshot. | | `externalReference` | string | Provider reference, normally Relay's `txId`. | | `virtualAccountId` | string or null | Provider virtual-account ID. | | `virtualAccountNumber` | string or null | Account number shown to the payer. | | `virtualAccountBankCode` | string or null | Bank code. | | `virtualAccountAccountName` | string or null | Beneficiary/account name. | | `virtualAccountCurrencyCode` | string or null | Account currency code. | | `virtualAccountStatus` | string or null | Provider account state. | | `virtualAccountValidFor` | integer or null | Effective validity in seconds. | | `virtualAccountAmountControl` | string or null | Provider amount-control mode. | | `virtualAccountAmount` | decimal string or null | Amount attached to the virtual account. | | `virtualAccountExpiryDate` | string or null | Provider expiry time. | | `virtualAccountCallbackUrl` | string or null | Provider/Relay callback route; do not invoke it from your integration. | | `settlementBankName` | string or null | Collection bank display name. | | `createdAt` | string | Detail creation time. | | `updatedAt` | string | Detail update time. | --- ## Statuses and assets Source: /docs/reference/statuses-and-assets # Statuses and assets Relay exposes an overall payment status, an independent settlement status, and—on reusable customer wallets—separate deposit and transfer statuses. Keep these state machines separate in your integration. ## Payment status | Value | Meaning | | --- | --- | | `pending` | The payment exists and has not reached a more specific funding state. | | `pending_verification` | Relay has evidence that requires additional verification. | | `awaiting_confirmation` | An observed transaction has not met the required finality threshold. | | `awaiting_offset` | Eligible confirmed funding is below the target and more funds are required. | | `successful` | The incoming payment target is funded. Outgoing settlement can still be pending. | | `completed` | Relay's post-funding workflow reached completion/submission. Check `settlementStatus` for final settlement evidence. | | `failed` | Payment processing reached a failed state. Inspect settlement independently if funds were already confirmed. | | `cancelled` | The payment was cancelled. | The exact transition path varies by payment type and network. Integrations should accept a valid state without assuming every payment visits every earlier state. ## Settlement status | Value | Meaning | | --- | --- | | `not_started` | No outgoing settlement has been submitted. | | `submitted` | Relay recorded an outgoing transaction and is checking its on-chain result. | | `settled` | Outgoing settlement was verified. | | `failed` | Settlement was verified as failed or exhausted safe processing attempts. | | `review_required` | The broadcast, resources, or accounting outcome is ambiguous and requires reconciliation before another send. | Some historical records can contain legacy settlement values. Treat unknown values as non-final, log them, and reconcile through retrieval or Relay support. ## Accounting state | Value | Meaning | | --- | --- | | `ready` | The payment's immutable target and cumulative confirmed accounting are ready for automated processing. | | `review_required` | Accounting evidence requires operational reconciliation. | ## Customer wallet deposit status | Value | Meaning | | --- | --- | | `confirmed` | Incoming evidence was credited and a merchant payout was queued. | | `settled` | The linked merchant payout was verified on-chain. | | `review_required` | The deposit or its payout cannot continue automatically. | ## Customer wallet transfer status | Value | Meaning | | --- | --- | | `queued` | Transfer is durably queued but not broadcast. | | `submitted` | An outgoing hash is recorded and awaiting verification. | | `settled` | On-chain settlement was verified. | | `review_required` | Relay will not authorize a replacement send until reconciliation is complete. | ## Enabled networks New wallet and payment work currently uses: | Network | API value | Customer wallets | Crypto payment intents | Fiat settlement | | --- | --- | --- | --- | --- | | TRON | `tron` | yes | yes | depends on active token configuration | | Solana | `solana` | yes | yes | no; crypto intents only | Ethereum, Polygon, and Stellar values can exist on historical records, but they are not enabled for new work in the current network policy. Do not infer availability from historical data or enum names. ## Tokens and precision Customer wallet requests accept `USDT` and `USDC`, subject to active network/token configuration. The broader stored token enum also contains `MATIC`, `TRON`, `ETH`, and `XLM` for historical or specialized records; their presence does not make them available for a new request. | Asset family | Expected precision in payment accounting | | --- | ---: | | USDT / USDC | 6 decimals | | XLM historical records | 7 decimals | | Other historical crypto assets | generally 18 decimals | Always use the active-assets endpoint and the payment's pinned `tokenDecimals`, `tokenContractAddress`, and chain fields when available. ## Decimal handling Payment-object monetary fields, customer deposit amounts, fees, merchant amounts, and transfer amounts are serialized as decimal strings. ```javascript import Decimal from "decimal.js"; const confirmed = new Decimal(payment.confirmedAmount); const target = new Decimal(payment.originalAmount); const fullyFunded = confirmed.greaterThanOrEqualTo(target); ``` Do not convert these values to JavaScript `number` before arithmetic. The standalone fiat-rate endpoint currently returns its indicative `rate` as a JSON number; convert it to your decimal type from its string representation and treat the created intent's stored quote as authoritative. ## Event ordering Webhook delivery order is not guaranteed. Use `resourceVersion` to avoid moving a local payment projection backward. A settlement event can arrive after a funded event, and a retry of the funded event can arrive after that newer settlement event. Event `id` handles duplicate delivery; `resourceVersion` handles resource ordering.