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.
How agents work
Section titled “How agents work”When you call a canister method through an agent, the agent:
- Encodes your arguments as Candid (a CBOR-wrapped binary format)
- Attaches a cryptographic identity (anonymous or authenticated)
- Sends a
POSTrequest to/api/v2/canister/<canister-id>/call(update) or/api/v2/canister/<canister-id>/query(query) - For update calls, polls the replica using
read_staterequests until the response is ready - Verifies the certificate in the response using the IC root key
- Decodes the Candid response into native language types
Query vs update calls
Section titled “Query vs update calls”The IC has two call types that agents route differently:
| Query | Update | |
|---|---|---|
| State changes | Not allowed | Allowed |
| Routing | Single replica: fast (~200ms) | Goes through consensus (~2–4 seconds) |
| Response verification | Node key signatures verified by default; certified data provides app-layer guarantees | Full certificate from consensus |
| Candid annotation | query | (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.
Available agents
Section titled “Available agents”DFINITY maintains official agents for JavaScript/TypeScript and Rust. Several community agents cover additional languages.
Official agents
Section titled “Official agents”JavaScript / TypeScript: @icp-sdk/core
The primary agent for browser and Node.js applications. Install from npm:
npm install @icp-sdk/coreImport 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:
cargo add ic-agentCrate documentation: docs.rs/ic-agent
Community agents
Section titled “Community agents”Community-maintained agents are available for Go, Java/Android, Dart/Flutter, .NET, Elixir, and C. See Developer Tools for the full list.
JavaScript / TypeScript: using the agent
Section titled “JavaScript / TypeScript: using the agent”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.
Generating bindings
Section titled “Generating bindings”Use @icp-sdk/bindgen to generate TypeScript bindings from a .did file:
npx @icp-sdk/bindgen --did-file ./backend.did --out-dir ./src/backend/apiFor Vite projects, use the Vite plugin to regenerate bindings automatically during development. See Candid and binding generation for details.
Creating an actor (browser)
Section titled “Creating an actor (browser)”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.
Creating an actor (Node.js)
Section titled “Creating an actor (Node.js)”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,});Making calls
Section titled “Making calls”Once you have an actor, call methods as regular async functions. The generated bindings handle Candid encoding and routing:
// Query call: fast, read-onlyconst greeting = await actor.greet("Ada");console.log(greeting); // "Hello, Ada!"Error handling
Section titled “Error handling”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); }}Rust: using ic-agent
Section titled “Rust: using ic-agent”Initializing the agent
Section titled “Initializing the agent”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(())}Making calls
Section titled “Making calls”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?;Authentication
Section titled “Authentication”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 discovery
Section titled “Canister discovery”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.
How it works
Section titled “How it works”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-caiPUBLIC_CANISTER_ID:frontend → bd3sg-teaaa-aaaaa-qaaba-caiFrontend: reading the cookie
Section titled “Frontend: reading the cookie”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 verificationThis works identically on local networks and mainnet without code changes.
Local development with a dev server
Section titled “Local development with a dev server”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:
const IC_ROOT_KEY_HEX = "308182..."; // placeholder: replace with your local replica root keyconst 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.
Authentication
Section titled “Authentication”Calls to ICP always carry a cryptographic identity. An anonymous identity is used by default.
Anonymous calls
Section titled “Anonymous calls”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 delegationconst agent = await HttpAgent.create({ host: "https://icp-api.io", identity, // DelegationIdentity from @icp-sdk/auth});Next steps
Section titled “Next steps”- Candid and binding generation: generate typed clients from
.didfiles - Inter-canister calls: canister-to-canister calls from within the IC
- Internet Identity: adding user authentication to offchain calls
- Asset canister: deploying the frontend that makes these calls