Skip to main content
Version: 0.96.0

Tracking Messages

Message Lifecycle

A CCIP message passes through three stages:

  1. Sent - Message emitted on source chain
  2. Committed - Merkle root committed on destination chain
  3. Executed - Message executed on destination chain

By Transaction Hash

Use getMessagesInTx when you have the source transaction hash. This is the fastest method.

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

By Message ID

Use getMessageById when you only have the message ID:

TypeScript
import { EVMChain } from '@chainlink/ccip-sdk'

const source = await EVMChain.fromUrl('https://rpc.sepolia.org')

const request = await source.getMessageById('0xabcd1234...')

console.log('Found message in tx:', request.tx.hash)
console.log('Status:', request.metadata?.status)

The getMessageById method uses the CCIP API for fast lookups by message ID.

By Sender Address

Use getMessagesForSender to find all messages from a specific sender:

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

Commit Status

Check if a message has been committed on the destination chain:

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]

const offRamp = await discoverOffRamp(source, dest, request.lane.onRamp)

const commit = await dest.getCommitReport({
commitStore: offRamp,
request,
})

if (commit) {
console.log('Committed at block:', commit.log.blockNumber)
console.log('Merkle root:', commit.report.merkleRoot)
} else {
console.log('Not yet committed')
}

Execution Status

Check if a committed message has been executed:

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,
commit,
})) {
console.log('Execution state:', ExecutionState[execution.receipt.state])
console.log('Gas used:', execution.receipt.gasUsed)

if (execution.receipt.state === ExecutionState.Success) {
console.log('Message successfully executed')
} else if (execution.receipt.state === ExecutionState.Failed) {
console.log('Message execution failed - may need manual execution')
}
}

Complete Example

Track a message through all stages:

TypeScript
import {
EVMChain,
discoverOffRamp,
ExecutionState
} from '@chainlink/ccip-sdk'

async function trackMessage(sourceTxHash: string) {
const source = await EVMChain.fromUrl('https://rpc.sepolia.org')
const dest = await EVMChain.fromUrl('https://rpc.fuji.avax.network')

// Stage 1: Get sent message
console.log('Fetching sent message...')
const requests = await source.getMessagesInTx(sourceTxHash)
const request = requests[0]
console.log('Message ID:', request.message.messageId)

const offRamp = await discoverOffRamp(source, dest, request.lane.onRamp)

// Stage 2: Check commit status
console.log('Checking commit status...')
const commit = await dest.getCommitReport({
commitStore: offRamp,
request,
})

if (!commit) {
console.log('Status: Pending commit')
return { status: 'pending_commit', request }
}
console.log('Committed at block:', commit.log.blockNumber)

// Stage 3: Check execution status
console.log('Checking execution status...')
for await (const execution of dest.getExecutionReceipts({
offRamp,
messageId: request.message.messageId,
commit,
})) {
if (execution.receipt.state === ExecutionState.Success) {
console.log('Status: Executed')
return { status: 'executed', request, commit, execution }
}
}

console.log('Status: Committed, pending execution')
return { status: 'pending_execution', request, commit }
}

const result = await trackMessage('0x1234...')

Message Status Enum

When using getMessageById, the metadata.status field indicates the current state:

TypeScript
import { MessageStatus } from '@chainlink/ccip-sdk'

const message = await chain.getMessageById(messageId)

if (message.metadata?.status === MessageStatus.Success) {
console.log('Transfer complete!')
}
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 by the CCIP network
VerifiedMessage has been verified by the CCIP network
SuccessMessage executed successfully on destination
FailedMessage execution failed on destination
UnknownAPI returned an unrecognized status

If you encounter MessageStatus.Unknown, update to the latest SDK version to handle new status values.

Generate links to view messages on the CCIP Explorer:

TypeScript
import { getCCIPExplorerUrl } from '@chainlink/ccip-sdk'

// Link by message ID
const messageUrl = getCCIPExplorerUrl('msg', messageId)
console.log('View message:', messageUrl)
// => 'https://ccip.chain.link/msg/0x...'

// Link by transaction hash
const txUrl = getCCIPExplorerUrl('tx', txHash)
console.log('View transaction:', txUrl)
// => 'https://ccip.chain.link/tx/0x...'

Polling for Status

Poll until a message reaches a final state:

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 message = await chain.getMessageById(messageId)
const status = message.metadata?.status ?? MessageStatus.Unknown

console.log('Current status:', MessageStatus[status])

// Check for final states
if (status === MessageStatus.Success || status === MessageStatus.Failed) {
return status
}

await new Promise(resolve => setTimeout(resolve, pollInterval))
} catch (error) {
// Message may not be indexed yet - keep polling
if (error instanceof CCIPMessageIdNotFoundError) {
console.log('Message not indexed yet, retrying...')
await new Promise(resolve => setTimeout(resolve, pollInterval))
continue
}
throw error
}
}

throw new Error('Timeout waiting for message completion')
}