For AI agents: Documentation index at /llms.txt

Skip to content

Calling from Clients

An agent is a client-side library that constructs ingress messages, signs them with a cryptographic identity, and sends them to ICP boundary nodes. Agents handle the protocol details (CBOR encoding, request IDs, certificate verification) so your application code works with native language types. An actor is a typed proxy for a specific canister, generated from its Candid interface and built on top of an agent. You interact with canisters through actors; the agent handles the underlying transport.

When you call a canister method through an agent, the agent:

  1. Encodes your arguments as Candid (a CBOR-wrapped binary format)
  2. Attaches a cryptographic identity (anonymous or authenticated)
  3. Sends a POST request to /api/v2/canister/<canister-id>/call (update) or /api/v2/canister/<canister-id>/query (query)
  4. For update calls, polls the replica using read_state requests until the response is ready
  5. Verifies the certificate in the response using the IC root key
  6. Decodes the Candid response into native language types

The IC has two call types that agents route differently:

QueryUpdate
State changesNot allowedAllowed
RoutingSingle replica: fast (~200ms)Goes through consensus (~2–4 seconds)
Response verificationNode key signatures verified by default; certified data provides app-layer guaranteesFull certificate from consensus
Candid annotationquery(default)

The Candid interface definition tells the agent which call type to use. When you generate typed bindings from a .did file, the generated code routes each method correctly: you do not need to decide manually.

DFINITY maintains official agents for JavaScript/TypeScript and Rust. Several community agents cover additional languages.

JavaScript / TypeScript: @icp-sdk/core

The primary agent for browser and Node.js applications. Install from npm:

Terminal window
npm install @icp-sdk/core

Import path: @icp-sdk/core/agent

Full documentation: js.icp.build

Rust: ic-agent

A low-level Rust library for building applications that interact with ICP. Add to your project:

Terminal window
cargo add ic-agent

Crate documentation: docs.rs/ic-agent

Community-maintained agents are available for Go, Java/Android, Dart/Flutter, .NET, Elixir, and C. See Developer Tools for the full list.

The recommended pattern is to generate typed bindings from your canister’s .did file and use the createActor helper those bindings export. This avoids writing raw agent calls and ensures your code matches the canister interface.

Use @icp-sdk/bindgen to generate TypeScript bindings from a .did file:

Terminal window
npx @icp-sdk/bindgen --did-file ./backend.did --out-dir ./src/backend/api

For Vite projects, use the Vite plugin to regenerate bindings automatically during development. See Candid and binding generation for details.

In a browser frontend served by an asset canister, read the canister ID from the environment cookie that icp-cli injects at deploy time:

import { createActor } from "./backend/api/backend";
import { getCanisterEnv } from "@icp-sdk/core/agent/canister-env";
// Declare the environment variables your asset canister exposes.
// icp-cli injects PUBLIC_CANISTER_ID:<name> for every canister in the project.
interface CanisterEnv {
readonly "PUBLIC_CANISTER_ID:backend": string;
}
const canisterEnv = getCanisterEnv<CanisterEnv>();
const canisterId = canisterEnv["PUBLIC_CANISTER_ID:backend"];
// Pass rootKey only on non-standard networks. On mainnet the IC root key is
// embedded in the agent: omit rootKey there.
// In local development, let the agent fetch the root key from the local replica.
const actor = createActor(canisterId, {
agentOptions: {
rootKey: !import.meta.env.DEV ? canisterEnv.IC_ROOT_KEY : undefined,
shouldFetchRootKey: import.meta.env.DEV,
},
});

getCanisterEnv reads the ic_env cookie that the asset canister sets automatically. See Canister discovery below for how this works.

For Node.js scripts and backend services, create an HttpAgent directly and pass it to createActor:

import { HttpAgent } from "@icp-sdk/core/agent";
import { createActor } from "./backend/api/backend";
const agent = await HttpAgent.create({
host: "https://icp-api.io",
// Omit identity to use the anonymous identity.
// Pass an identity here for authenticated calls.
// IC root key is embedded in the agent for mainnet: do not set shouldFetchRootKey.
});
const actor = createActor("<canister-id>", { agent });

For local development against a local replica, fetch the root key:

const agent = await HttpAgent.create({
host: "http://127.0.0.1:8000",
shouldFetchRootKey: true,
});

Once you have an actor, call methods as regular async functions. The generated bindings handle Candid encoding and routing:

// Query call: fast, read-only
const greeting = await actor.greet("Ada");
console.log(greeting); // "Hello, Ada!"

Agent errors are thrown as Error instances. Wrap calls in try/catch:

try {
const result = await actor.greet("Ada");
} catch (err) {
if (err instanceof Error) {
console.error("Call failed:", err.message);
}
}
use anyhow::Result;
use ic_agent::Agent;
pub async fn create_agent(url: &str, use_mainnet: bool) -> Result<Agent> {
let agent = Agent::builder().with_url(url).build()?;
if !use_mainnet {
// Fetch the root key for local development only.
// The mainnet root key is embedded in the agent.
agent.fetch_root_key().await?;
}
Ok(agent)
}
#[tokio::main]
async fn main() -> Result<()> {
let agent = create_agent("https://ic0.app", true).await?;
Ok(())
}

Use agent.query for query calls and agent.update for update calls. Encode arguments with candid::Encode! and decode responses with candid::Decode!:

use ic_agent::{Agent, export::Principal};
use candid::{Encode, Decode, CandidType};
use serde::Deserialize;
async fn call_greet(agent: &Agent, canister_id: &str) -> anyhow::Result<String> {
let canister = Principal::from_text(canister_id)?;
// Query call: encode the argument, call the method, decode the response
let response = agent
.query(&canister, "greet")
.with_arg(Encode!(&"Ada")?)
.call()
.await?;
let (greeting,) = Decode!(&response, String)?;
Ok(greeting)
}

For update calls, use .call_and_wait() instead of .call():

let response = agent
.update(&canister, "update_name")
.with_arg(Encode!(&"Ada")?)
.call_and_wait() // submits the update and polls until the response is certified
.await?;

The Rust agent uses an Identity to sign requests. The default is anonymous. To authenticate:

use ic_agent::identity::BasicIdentity;
let identity = BasicIdentity::from_pem_file("path/to/identity.pem")?;
let agent = Agent::builder()
.with_url("https://ic0.app")
.with_identity(identity)
.build()?;

Available identity types: AnonymousIdentity, BasicIdentity (Ed25519), Secp256k1Identity, Prime256v1Identity. See ic_agent::identity for the full list.

Canister IDs differ between environments (local, staging, mainnet). Hardcoding them breaks when you redeploy or share code. icp-cli solves this with automatic canister ID injection.

During icp deploy, icp-cli injects PUBLIC_CANISTER_ID:<canister-name> environment variables into every canister in the project. For a project with backend and frontend canisters, every canister receives:

PUBLIC_CANISTER_ID:backend → bkyz2-fmaaa-aaaaa-qaaaq-cai
PUBLIC_CANISTER_ID:frontend → bd3sg-teaaa-aaaaa-qaaba-cai

The asset canister exposes these variables via an ic_env cookie, along with the network’s root key (IC_ROOT_KEY). Use getCanisterEnv from @icp-sdk/core to read the cookie:

import { getCanisterEnv } from "@icp-sdk/core/agent/canister-env";
// Declare the environment variables your asset canister exposes.
// icp-cli injects PUBLIC_CANISTER_ID:<name> for every canister in the project.
interface CanisterEnv {
readonly "PUBLIC_CANISTER_ID:backend": string;
}
const env = getCanisterEnv<CanisterEnv>();
const backendId = env["PUBLIC_CANISTER_ID:backend"];
const rootKey = env.IC_ROOT_KEY; // Uint8Array: use for certificate verification

This works identically on local networks and mainnet without code changes.

During development, your dev server runs outside the asset canister and the ic_env cookie is not set automatically. Simulate it by configuring your dev server to inject the cookie. With Vite:

vite.config.ts
const IC_ROOT_KEY_HEX = "308182..."; // placeholder: replace with your local replica root key
const BACKEND_CANISTER_ID = "bkyz2-fmaaa-aaaaa-qaaaq-cai"; // from `icp canister list`
export default defineConfig({
server: {
headers: {
"Set-Cookie": `ic_env=${encodeURIComponent(
`ic_root_key=${IC_ROOT_KEY_HEX}&PUBLIC_CANISTER_ID:backend=${BACKEND_CANISTER_ID}`
)}; SameSite=Lax;`,
},
},
});

The hello-world template from icp new includes this setup. See the template’s vite.config.ts for a working example.

Calls to ICP always carry a cryptographic identity. An anonymous identity is used by default.

Anonymous calls work without any setup. The sender principal is "2vxsx-fae". Canisters can check the caller principal to detect anonymous calls.

Authenticated calls with Internet Identity

Section titled “Authenticated calls with Internet Identity”

To associate calls with a user’s Internet Identity, use @icp-sdk/auth to complete the delegation flow and get an Identity object, then pass it to the agent. See Internet Identity for the full integration guide.

Once you have an authenticated identity, pass it to the agent at creation time:

import { HttpAgent } from "@icp-sdk/core/agent";
// identity obtained from Internet Identity delegation
const agent = await HttpAgent.create({
host: "https://icp-api.io",
identity, // DelegationIdentity from @icp-sdk/auth
});