Tracking Messages
Two Approaches
The SDK provides two ways to track messages. Choose based on your architecture:
| Approach | Data source | What you need | What you get |
|---|---|---|---|
| API-based | CCIP API | Message ID or tx hash | Lifecycle status, delivery time, execution receipt hash |
| RPC-based | On-chain logs | Source + dest RPCs, OffRamp address | Commit reports, execution state events |
Use the API approach for most integrations — it covers the full lifecycle with a single call and requires only one RPC connection.
Use the RPC approach when you need to bypass the API entirely (e.g., apiClient: null for fully decentralized operation) or when you need raw on-chain data like Merkle roots or commit logs.
Message Lifecycle
A CCIP message passes through these stages:
- Sent —
CCIPMessageSentevent emitted on source chain - Committed/Verified — Message committed (Merkle root) or verified (cross-chain verifiers) on destination chain
- Executed —
ExecutionStateChangedevent on destination chain (state:SuccessorFailed)
API-Based Tracking
By Message ID
getMessageById queries the CCIP API. It returns the message with metadata containing lifecycle status, execution details, and delivery timing. Requires the CCIP API (enabled by default).
import { EVMChain, MessageStatus } from '@chainlink/ccip-sdk'
const chain = await EVMChain.fromUrl('https://rpc.sepolia.org')
const request = await chain.getMessageById('0xabcd1234...')
// metadata is populated by the API
console.log('Status:', request.metadata.status) // e.g., MessageStatus.Success
console.log('Source chain:', request.metadata.sourceNetworkInfo.name)
console.log('Dest chain:', request.metadata.destNetworkInfo.name)
if (request.metadata.status === MessageStatus.Success) {
console.log('Execution tx:', request.metadata.receiptTransactionHash)
console.log('Delivery time:', request.metadata.deliveryTime, 'ms')
}
if (request.metadata.readyForManualExecution) {
console.log('Message can be manually executed')
}
getMessageById throws CCIPApiClientNotAvailableError if you created the chain with apiClient: null. It throws CCIPMessageIdNotFoundError (transient) if the message is not yet indexed — retry after a few seconds.
Polling Until Final State
Poll getMessageById until the message reaches Success or Failed:
import { EVMChain, MessageStatus, CCIPMessageIdNotFoundError } from '@chainlink/ccip-sdk'
async function pollMessageStatus(
chain: EVMChain,
messageId: string,
timeoutMs = 30 * 60 * 1000 // 30 minutes
): Promise<MessageStatus> {
const startTime = Date.now()
const pollInterval = 10000 // 10 seconds
while (Date.now() - startTime < timeoutMs) {
try {
const request = await chain.getMessageById(messageId)
const status = request.metadata.status
console.log('Current status:', status)
if (status === MessageStatus.Success || status === MessageStatus.Failed) {
return status
}
await new Promise(resolve => setTimeout(resolve, pollInterval))
} catch (error) {
if (error instanceof CCIPMessageIdNotFoundError) {
// Message not indexed yet — expected for recently sent messages
await new Promise(resolve => setTimeout(resolve, pollInterval))
continue
}
throw error
}
}
throw new Error('Timeout waiting for message completion')
}
MessageStatus Values
The metadata.status field reflects the API's view of the message lifecycle:
| Status | Description |
|---|---|
Sent | Message sent on source chain, pending finalization |
SourceFinalized | Source chain transaction finalized |
Committed | Commit report accepted on destination chain |
Blessed | Commit blessed by Risk Management Network |
Verifying | Message is being verified |
Verified | Message verified |
Success | Executed successfully on destination (final) |
Failed | Execution failed on destination (final — may need manual execution) |
Unknown | API returned an unrecognized status — update to the latest SDK version |
RPC-Based Tracking
These methods read on-chain logs directly. They work with apiClient: null and do not depend on the CCIP API.
Get Messages from a Transaction
getMessagesInTx decodes CCIPMessageSent events from a source transaction's logs. This is the fastest way to get message details right after sending.
import { EVMChain } from '@chainlink/ccip-sdk'
const source = await EVMChain.fromUrl('https://rpc.sepolia.org')
const requests = await source.getMessagesInTx('0x1234...')
for (const request of requests) {
console.log('Message ID:', request.message.messageId)
console.log('Sequence Number:', request.message.sequenceNumber)
console.log('Destination:', request.lane.destChainSelector)
}
getMessagesInTx reads from RPC first. If RPC decoding fails and the CCIP API is enabled, it falls back to the API automatically. The returned CCIPRequest does not include metadata unless the API fallback is used.
Search by Sender Address
getMessagesForSender scans on-chain logs for all messages from a specific sender. RPC-only — no API dependency.
import { EVMChain, getMessagesForSender } from '@chainlink/ccip-sdk'
const source = await EVMChain.fromUrl('https://rpc.sepolia.org')
for await (const request of getMessagesForSender(
source,
'0xSenderAddress...',
{ startBlock: 1000000 }
)) {
console.log('Message ID:', request.message.messageId)
console.log('Sent to:', request.lane.destChainSelector)
}
getMessagesForSender yields partial requests — tx and timestamp fields are omitted since it scans logs without fetching full transaction data. To get the full CCIPRequest, call getMessagesInTx with the message's transaction hash.
Check Commit/Verification Status
getVerifications scans destination chain logs for commit reports. Use this when you need on-chain proof that a message was committed — for example, before manual execution via the step-by-step workflow.
Requires both source and destination RPCs, plus the OffRamp address:
import { EVMChain, discoverOffRamp } from '@chainlink/ccip-sdk'
const source = await EVMChain.fromUrl('https://rpc.sepolia.org')
const dest = await EVMChain.fromUrl('https://rpc.fuji.avax.network')
const requests = await source.getMessagesInTx('0x1234...')
const request = requests[0]
// discoverOffRamp reads from source + dest RPCs (result is memoized)
const offRamp = await discoverOffRamp(source, dest, request.lane.onRamp)
try {
const verifications = await dest.getVerifications({ offRamp, request })
// CCIPVerifications is a union — narrow before accessing fields
if ('report' in verifications) {
// Merkle-based commit
console.log('Committed at block:', verifications.log.blockNumber)
console.log('Merkle root:', verifications.report.merkleRoot)
} else {
// Cross-chain verifier based
console.log('Required CCVs:', verifications.verificationPolicy.requiredCCVs)
console.log('Verifications:', verifications.verifications.length)
}
} catch (error) {
// Throws CCIPCommitNotFoundError (transient) when not yet committed — retry later
console.log('Not yet committed')
}
Check Execution Status
getExecutionReceipts scans ExecutionStateChanged events on the destination chain OffRamp. RPC-only.
import { EVMChain, ExecutionState } from '@chainlink/ccip-sdk'
const dest = await EVMChain.fromUrl('https://rpc.fuji.avax.network')
for await (const execution of dest.getExecutionReceipts({
offRamp,
messageId: request.message.messageId,
})) {
switch (execution.receipt.state) {
case ExecutionState.Success:
console.log('Executed successfully')
if (execution.receipt.gasUsed != null) {
console.log('Gas used:', execution.receipt.gasUsed)
}
break
case ExecutionState.Failed:
console.log('Execution failed — may need manual execution')
console.log('Return data:', execution.receipt.returnData)
break
case ExecutionState.InProgress:
console.log('Execution in progress')
break
}
}
Complete RPC-Based Example
Track a message through all stages without the CCIP API:
import {
EVMChain,
discoverOffRamp,
ExecutionState
} from '@chainlink/ccip-sdk'
async function trackMessage(sourceTxHash: string) {
// apiClient: null — fully decentralized, no API dependency
const source = await EVMChain.fromUrl('https://rpc.sepolia.org', { apiClient: null })
const dest = await EVMChain.fromUrl('https://rpc.fuji.avax.network', { apiClient: null })
// Stage 1: Get sent message (RPC — decode source tx logs)
const requests = await source.getMessagesInTx(sourceTxHash)
const request = requests[0]
console.log('Message ID:', request.message.messageId)
// Discover OffRamp on dest chain (RPC — contract calls, memoized)
const offRamp = await discoverOffRamp(source, dest, request.lane.onRamp)
// Stage 2: Check commit status (RPC — scan dest chain logs)
let verifications
try {
verifications = await dest.getVerifications({ offRamp, request })
} catch {
// CCIPCommitNotFoundError — not yet committed
return { status: 'pending_commit', request }
}
if ('report' in verifications) {
console.log('Committed at block:', verifications.log.blockNumber)
}
// Stage 3: Check execution status (RPC — scan dest chain logs)
for await (const execution of dest.getExecutionReceipts({
offRamp,
messageId: request.message.messageId,
})) {
if (execution.receipt.state === ExecutionState.Success) {
return { status: 'executed', request, verifications, execution }
}
if (execution.receipt.state === ExecutionState.Failed) {
return { status: 'failed', request, verifications, execution }
}
}
return { status: 'pending_execution', request, verifications }
}
CCIP Explorer Links
Generate links to the CCIP Explorer (no API or RPC needed):
import { getCCIPExplorerUrl } from '@chainlink/ccip-sdk'
const messageUrl = getCCIPExplorerUrl('msg', messageId)
// => 'https://ccip.chain.link/msg/0x...'
const txUrl = getCCIPExplorerUrl('tx', txHash)
// => 'https://ccip.chain.link/tx/0x...'
Method Reference
| Method | Data source | Requires API | Returns |
|---|---|---|---|
getMessageById(id) | CCIP API | Yes | CCIPRequest with metadata (status, delivery time, execution receipt) |
getMessagesInTx(txHash) | RPC (API fallback) | No | CCIPRequest[] (no metadata unless API fallback used) |
getMessagesForSender(chain, sender, filter) | RPC (log scan) | No | Partial CCIPRequest (no tx, timestamp, or metadata) |
getVerifications({offRamp, request}) | RPC (log scan) | No | CCIPVerifications (commit report or verifier results) |
getExecutionReceipts({offRamp, messageId}) | RPC (log scan) | No | CCIPExecution iterator (state, gasUsed, return data) |
discoverOffRamp(source, dest, onRamp) | RPC (contract calls) | No | OffRamp address (memoized) |
getCCIPExplorerUrl(type, value) | None | No | URL string |
Related
- Manual Execution — Execute stuck or failed messages
- Sending Messages — Send cross-chain messages
- Error Handling — Handle failures and retry patterns