Skip to main content
Version: 1.1.1

Tracking Messages

Two Approaches

The SDK provides two ways to track messages. Choose based on your architecture:

ApproachData sourceWhat you needWhat you get
API-basedCCIP APIMessage ID or tx hashLifecycle status, delivery time, execution receipt hash
RPC-basedOn-chain logsSource + dest RPCs, OffRamp addressCommit 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:

  1. SentCCIPMessageSent event emitted on source chain
  2. Committed/Verified — Message committed (Merkle root) or verified (cross-chain verifiers) on destination chain
  3. ExecutedExecutionStateChanged event on destination chain (state: Success or Failed)

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).

TypeScript
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')
}
note

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:

TypeScript
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:

StatusDescription
SentMessage sent on source chain, pending finalization
SourceFinalizedSource chain transaction finalized
CommittedCommit report accepted on destination chain
BlessedCommit blessed by Risk Management Network
VerifyingMessage is being verified
VerifiedMessage verified
SuccessExecuted successfully on destination (final)
FailedExecution failed on destination (final — may need manual execution)
UnknownAPI 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.

TypeScript
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)
}
note

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.

TypeScript
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:

TypeScript
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.

TypeScript
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:

TypeScript
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 }
}

Generate links to the CCIP Explorer (no API or RPC needed):

TypeScript
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

MethodData sourceRequires APIReturns
getMessageById(id)CCIP APIYesCCIPRequest with metadata (status, delivery time, execution receipt)
getMessagesInTx(txHash)RPC (API fallback)NoCCIPRequest[] (no metadata unless API fallback used)
getMessagesForSender(chain, sender, filter)RPC (log scan)NoPartial CCIPRequest (no tx, timestamp, or metadata)
getVerifications({offRamp, request})RPC (log scan)NoCCIPVerifications (commit report or verifier results)
getExecutionReceipts({offRamp, messageId})RPC (log scan)NoCCIPExecution iterator (state, gasUsed, return data)
discoverOffRamp(source, dest, onRamp)RPC (contract calls)NoOffRamp address (memoized)
getCCIPExplorerUrl(type, value)NoneNoURL string