Tracking a Transfer
Prerequisites
Before tracking a transfer, you need to send it. See Sending a Transfer to get the Transfer ID.
Transfer ID
Technical Details
Transfer ID is the transaction hash of the event on the Proxy contract.
┌─────────────────────────────────────────────────────────────────┐
│ Proxy Contract │
│ │
│ 1. Receives tokens via transferNotification() │
│ 2. Calls _emitTvmEvent() or _deployEvmEvent() │
│ 3. Emits TvmTvmNative or TvmTvmAlien event │
│ 4. Transaction hash of this event = TRANSFER ID │
└─────────────────────────────────────────────────────────────────┘Location in Blockchain
Transaction (on Proxy contract) ← Transaction Hash = TRANSFER ID
├── In Message (incoming tokens)
├── Out Messages
│ ├── Internal Messages (internal transfers)
│ └── External Out Message ← EVENT (TvmTvmNative/TvmTvmAlien)
└── Account (updated state)Event Types
| Event | TokenType | Description |
|---|---|---|
TvmTvmNative | Native | Native token transfer (lock → mint) |
TvmTvmAlien | Alien | Alien token transfer (burn → unlock) |
Transfer Statuses
Two Status Levels
The system has two status levels:
| Level | Where Stored | Purpose |
|---|---|---|
| TransferStatus | Bridge Aggregator API | Aggregated status for UI/integrations |
| Event Contract Status | On-chain (Event contract) | Detailed event verification state |
Relationship: Bridge Aggregator API aggregates on-chain Event contract statuses into a simplified TransferStatus:
Event Contract Status → TransferStatus (API)
─────────────────────────────────────────────────────────────────────
Initializing, Pending, Verified → Pending
Confirmed, LiquidityProvided → Completed
Rejected, Cancelled → FailedTransferStatus (API level)
Used in Bridge Aggregator API responses (/v2/transfers/status, /v2/transfers/search).
| Status | Code | Description |
|---|---|---|
Pending | 1 | Event created, awaiting confirmation in destination network |
Completed | 2 | Transfer fully completed (tokens delivered) |
Failed | 3 | Transfer rejected by relays |
Event Contract Status (on-chain level)
Detailed Event contract statuses in the blockchain. Used to understand the verification stage of the event.
| Status | Code | Description | → TransferStatus |
|---|---|---|---|
Initializing | 0 | Event contract deployed | Pending |
Pending | 1 | Awaiting verification | Pending |
Confirmed | 2 | Confirmed, proxy called, tokens delivered | Completed |
Rejected | 3 | Rejected | Failed |
Cancelled | 4 | Cancelled by user | Failed |
LimitReached | 5 | Daily limit exceeded, requires liquidity | Pending |
LiquidityRequested | 6 | Liquidity request created | Pending |
LiquidityProvided | 7 | Liquidity provided, tokens delivered | Completed |
Verified | 8 | Merkle proof verified, awaiting token deploy | Pending |
Trustless transition: Pending → Verified → Confirmed
Status Transition Diagram

Getting Status by Transfer ID
API Endpoint
POST https://tetra-history-api.chainconnect.com/v2/transfers/status
Content-Type: application/jsonTest Environment
For testing, use https://history-api-test.chainconnect.com/v2/transfers/status
Request Parameters
Important
According to the OpenAPI specification, the field is called outgoingTransactionHash (not outgoingMessageHash)
| Parameter | Type | Required | Description |
|---|---|---|---|
outgoingTransactionHash | string | ✅ | Transfer ID (hex, 64 characters) |
dappChainId | number | ✅ | Chain ID of source network (TON = -239, Tycho = 2000) |
timestampCreatedFrom | number | ❌ | Unix timestamp for filtering (optional) |
Request Example (TvmTvm)
curl -X POST 'https://tetra-history-api.chainconnect.com/v2/transfers/status' \
-H 'Content-Type: application/json' \
-d '{
"tvmTvm": {
"outgoingTransactionHash": "abc123def456...",
"dappChainId": -239,
"timestampCreatedFrom": null
}
}'Response Example
{
"transfer": {
"tvmTvm": {
"transferStatus": "Pending",
"timestampCreatedAt": 1767972717,
"outgoing": {
"tokenType": "Native",
"contractAddress": "aaaa...1111",
"chainId": -239,
"userAddress": "0:3333...cccc",
"tokenAddress": "0:1111...aaaa",
"proxyAddress": "0:6666...ffff",
"volumeExec": "0.1000",
"volumeUsdtExec": "0",
"feeVolumeExec": "0",
"messageHash": "abc123def456...",
"transactionHash": "def789abc012..."
},
"incoming": {
"tokenType": null,
"contractAddress": null,
"chainId": 2000,
"userAddress": "0:2222...bbbb",
"tokenAddress": null,
"proxyAddress": null,
"volumeExec": null,
"volumeUsdtExec": null,
"feeVolumeExec": null,
"messageHash": null,
"transactionHash": null
}
}
},
"notInstantTransfer": null,
"proofPayload": {
"txBlockProof": "te6ccgECCg...",
"txProof": "te6ccgEBBw...",
"messageHash": "abc123def456...",
"outMessageIndex": 0,
"event": {
"tokenType": "Native",
"chainId": 2000,
"token": "0:1111...aaaa",
"amount": "99000",
"recipient": "0:2222...bbbb",
"value": "150000000",
"expectedGas": "0",
"remainingGasTo": "0:3333...cccc",
"sender": "0:3333...cccc",
"payload": "te6ccgEBAQEAAgAAAA==",
"nativeProxyWallet": "0:9999...2222",
"name": "Tether USD",
"symbol": "USD₮",
"decimals": 6
},
"feeAmount": "1000"
}
}Response Field Descriptions
transfer.tvmTvm — transfer data
| Field | Type | Description |
|---|---|---|
transferStatus | string | Status: "Pending", "Completed", "Failed" |
timestampCreatedAt | number | Unix timestamp of transfer creation |
outgoing / incoming — transfer side data
| Field | Type | Description |
|---|---|---|
tokenType | string | null | "Native" or "Alien" |
contractAddress | string | null | Event contract address (hex without 0:) |
chainId | number | Network Chain ID |
userAddress | string | User address (sender/recipient) |
tokenAddress | string | null | Token address in this network |
proxyAddress | string | null | Proxy contract address |
volumeExec | string | null | Amount in human-readable format |
volumeUsdtExec | string | null | Amount in USDT equivalent |
feeVolumeExec | string | null | Fee charged |
messageHash | string | null | Event message hash |
transactionHash | string | null | Transaction hash (Transfer ID for outgoing) |
proofPayload — data for non-credit transfers
| Field | Type | Description |
|---|---|---|
txBlockProof | string | Block proof (BOC base64) |
txProof | string | Transaction Merkle proof (BOC base64) |
messageHash | string | Event message hash |
outMessageIndex | number | Index of outgoing message in transaction |
event | object | Event data (see below) |
feeAmount | string | null | Fee in nano-units |
proofPayload.event — event data
| Field | Type | Description |
|---|---|---|
tokenType | string | "Native" or "Alien" |
chainId | number | Destination network Chain ID |
token | string | Token address |
amount | string | Amount in nano-units |
recipient | string | Recipient address |
value | string | Attached value (gas) |
expectedGas | string | Expected gas |
remainingGasTo | string | Address for gas return |
sender | string | Sender address |
payload | string | Additional payload (BOC base64) |
name | string | Token name |
symbol | string | Token symbol |
decimals | number | Number of decimals |
notInstantTransfer — liquidity request data
Populated when the transfer requires liquidity (LimitReached → LiquidityRequested status).
| Field | Type | Description |
|---|---|---|
liquidityRequestAddress | string | Liquidity request contract address |
status | string | Request status |
amount | string | Requested amount |
Response Interpretation
| Field | Value | Action |
|---|---|---|
transferStatus: "Pending" | Waiting | Continue polling |
transferStatus: "Completed" | Completed | Success! |
transferStatus: "Failed" | Error | Handle error |
proofPayload != null | Proof ready | Can deploy event (non-credit) |
incoming.transactionHash != null | Second part executed | Transfer almost completed |
Transfer History
API Endpoint
POST https://tetra-history-api.chainconnect.com/v2/transfers/search
Content-Type: application/jsonRequest Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
userAddresses | string[] | ❌ | Filter by user addresses (max 7) |
transferKinds | string[] | ❌ | Transfer type: ["TvmToTvm"] |
statuses | string[] | ❌ | Filter by statuses: ["Pending", "Completed", "Failed"] |
fromTvmChainId | number | ❌ | Source network Chain ID |
toTvmChainId | number | ❌ | Destination network Chain ID |
createdAtGe | number | ❌ | Unix timestamp >= (from date) |
createdAtLe | number | ❌ | Unix timestamp <= (to date) |
ordering | string | ❌ | Sorting: "CreatedAtAscending" or "CreatedAtDescending" |
limit | number | ✅ | Record limit (max count in response) |
offset | number | ✅ | Offset (for pagination) |
isNeedTotalCount | boolean | ✅ | Whether to return total transfer count |
Request Example
curl -X POST 'https://tetra-history-api.chainconnect.com/v2/transfers/search' \
-H 'Content-Type: application/json' \
-d '{
"userAddresses": ["0:3333...cccc"],
"transferKinds": ["TvmToTvm"],
"statuses": ["Pending", "Completed"],
"fromTvmChainId": -239,
"toTvmChainId": 2000,
"ordering": "CreatedAtDescending",
"limit": 10,
"offset": 0,
"isNeedTotalCount": true
}'Response Example
{
"transfers": [
{
"tvmTvm": {
"transferStatus": "Completed",
"timestampCreatedAt": 1767972717,
"outgoing": {
"tokenType": "Native",
"contractAddress": "aaaa...1111",
"chainId": -239,
"userAddress": "0:3333...cccc",
"tokenAddress": "0:1111...aaaa",
"proxyAddress": "0:6666...ffff",
"volumeExec": "1.0000",
"transactionHash": "abc123..."
},
"incoming": {
"tokenType": "Alien",
"contractAddress": "bbbb...2222",
"chainId": 2000,
"userAddress": "0:2222...bbbb",
"tokenAddress": "0:4444...dddd",
"proxyAddress": "0:7777...0000",
"volumeExec": "0.9990",
"transactionHash": "def456..."
}
}
}
],
"totalCount": 42
}Transfer ID for each transfer: transfers[i].tvmTvm.outgoing.transactionHash
Tracking Algorithm
┌─────────────────────────────────────────────────────────────────────────┐
│ 1. GET TRANSFER ID │
│ After sending tokens to the Proxy contract, save the transaction │
│ hash of that transaction — this is the Transfer ID │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ 2. REQUEST STATUS │
│ POST /v2/transfers/status with Transfer ID and Chain ID │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ 3. CHECK transferStatus │
│ │
│ "Pending" → Transfer in progress, retry request in 5-10 sec │
│ "Completed" → Success! Tokens delivered to recipient │
│ "Failed" → Error, transfer rejected │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ 4. FOR COMPLETED TRANSFER │
│ incoming.transactionHash — hash of token delivery transaction │
│ incoming.volumeExec — received amount (after fees) │
└─────────────────────────────────────────────────────────────────────────┘What to Check in Response
| Field | Meaning |
|---|---|
transferStatus: "Pending" | Transfer in progress — retry request |
transferStatus: "Completed" | Transfer completed — tokens delivered |
transferStatus: "Failed" | Transfer rejected — handle error |
incoming.transactionHash != null | Delivery transaction executed |
proofPayload != null | Merkle proof ready (for non-credit transfers) |
Credit vs Non-credit Transfers
| Type | Description | User Actions |
|---|---|---|
| Credit | Relays automatically deploy event and deliver tokens | Only track status |
| Non-credit | User deploys event contract themselves | Wait for proofPayload, deploy event |
For non-credit transfers:
- Wait for
proofPayloadto appear in response - Use
proofPayload.txBlockProofandproofPayload.txProofto deploy Event contract in destination network - After deploying Event contract, the transfer will complete automatically
Example: TypeScript polling
async function waitForTransferCompletion(
transferId: string,
chainId: number,
maxAttempts = 60,
initialDelayMs = 5000
): Promise<TransferStatusResponse> {
let delay = initialDelayMs;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
const response = await fetch('https://tetra-history-api.chainconnect.com/v2/transfers/status', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
tvmTvm: {
outgoingTransactionHash: transferId,
dappChainId: chainId,
},
}),
});
const status = await response.json();
if (status.transfer.tvmTvm?.transferStatus === 'Completed') {
console.log(`Transfer completed after ${attempt} attempts`);
return status;
}
if (status.transfer.tvmTvm?.transferStatus === 'Failed') {
throw new Error('Transfer failed');
}
console.log(`Attempt ${attempt}: status = ${status.transfer.tvmTvm?.transferStatus}`);
await new Promise(resolve => setTimeout(resolve, delay));
delay = Math.min(delay * 1.2, 30000); // Max 30 seconds
}
throw new Error('Transfer timeout');
}
// Usage
const transferId = 'abc123def456...'; // Transfer ID from Step 3 of sending a transfer
const result = await waitForTransferCompletion(transferId, -239);
console.log('Transfer completed:', result.transfer.tvmTvm?.incoming);Getting Status from Contract (on-chain)
In addition to Bridge Aggregator API, transfer status can be obtained directly from the Event contract in the blockchain. This is useful for:
- Verifying API data
- Working without dependency on centralized API
- Getting detailed on-chain status
Event Contract Address
Event contract address can be obtained from API response:
transfer.tvmTvm.outgoing.contractAddress— hex address without0:prefix
Reading Status from Contract
import { TonClient, Address } from '@ton/ton';
// ABI for getDetails method of Event contract
const EVENT_CONTRACT_ABI = {
getDetails: {
name: 'getDetails',
inputs: [],
outputs: [
{ name: 'status', type: 'uint8' },
{ name: 'sender', type: 'address' },
{ name: 'recipient', type: 'address' },
{ name: 'amount', type: 'uint128' },
// ... other fields
],
},
};
// Mapping numeric statuses to strings
const EVENT_STATUS = {
0: 'Initializing',
1: 'Pending',
2: 'Confirmed',
3: 'Rejected',
4: 'Cancelled',
5: 'LimitReached',
6: 'LiquidityRequested',
7: 'LiquidityProvided',
8: 'Verified',
} as const;
async function getEventContractStatus(
client: TonClient,
eventContractAddress: string
): Promise<{ status: string; statusCode: number }> {
const address = Address.parse(`0:${eventContractAddress}`);
// Call contract get-method
const result = await client.runMethod(address, 'getDetails');
// Parse result
const statusCode = result.stack.readNumber();
const status = EVENT_STATUS[statusCode as keyof typeof EVENT_STATUS] || 'Unknown';
return { status, statusCode };
}
// Usage
const client = new TonClient({
endpoint: 'https://toncenter.com/api/v2/jsonRPC',
});
// contractAddress from API response (without 0:)
const eventContractAddress = 'aaaa...1111';
const { status, statusCode } = await getEventContractStatus(client, eventContractAddress);
console.log(`Event status: ${status} (code: ${statusCode})`);Full Event Contract Data
To get all event data, use the getDetails method:
interface EventDetails {
status: number;
sender: string;
recipient: string;
amount: bigint;
token: string;
chainId: number;
// ... other fields depend on event type
}
async function getFullEventDetails(
client: TonClient,
eventContractAddress: string
): Promise<EventDetails> {
const address = Address.parse(`0:${eventContractAddress}`);
const result = await client.runMethod(address, 'getDetails');
return {
status: result.stack.readNumber(),
sender: result.stack.readAddress().toString(),
recipient: result.stack.readAddress().toString(),
amount: result.stack.readBigNumber(),
token: result.stack.readAddress().toString(),
chainId: result.stack.readNumber(),
};
}Important
The getDetails response structure may differ for different Event contract versions (Native vs Alien). Check the contract ABI before parsing.