Skip to main content
This page covers two separate things that are easy to conflate: an agent authenticating to T3N (proving who it is), and an agent being authorized to call a specific contract function (proving it’s allowed to).
This is about agents acting through your contract. If you’re looking for how you, the tenant developer, authenticate to manage your own deployment, see Set Up Development Environment instead — that’s a different session.This page assumes the agent already has a DID. Creating that identity in the first place — minting the agent and publishing its agent card — is Agent Onboarding, which happens before anything on this page.That identity also needs its own test credits before any metered call on this page will work — an agent DID’s balance is separate from its tenant’s and starts at zero. Get it a key from the same claim page you used for your own; it comes with credits attached.

1. An agent authenticates like any other T3N session

An agent has its own identity — its own key pair and its own DID — separate from the tenant that owns the contract it’s calling. It authenticates the same way any T3N client does: handshake, then authenticate, then read its own DID back from the session. There’s nothing tenant-specific to configure here.
Never hard-code or derive an agent’s DID any more than you would a tenant’s — always read it back from the authenticated session, exactly as agentDid is read above. And generate AGENT_KEY as its own separate credential (the same way you’d generate any Ethereum-style keypair) — don’t reuse your tenant’s T3N_API_KEY for an agent.

2. Being authenticated is not being authorized

Authenticating proves the agent’s identity. It does not grant it permission to call anything. Before an agent can invoke a contract function — especially one that makes an outbound HTTP call — the user who owns the data (the “data owner,” not the agent, and not you as the tenant developer) has to explicitly grant that agent access:
member-delegation-update replaces the member’s entire delegation document. Whatever you pass as grants becomes the complete new policy, so a single-grant write like the one above drops every other agent’s grants, and grants: [] revokes all delegated access. To change one grant while leaving the rest intact, read the current policy first (member-delegation-get) and write back the merged list — or let the SDK do it for you.The same applies to discover_dids: it is a document-level field, so a write that omits it clears the member’s whole discovery list. Read-merge-write, or omit it deliberately.
To add or change a single grant safely, use the SDK’s updateMemberDelegation. It reads the current policy, merges your grant by (grantee, contract_id), and writes the whole document back, so every other grant survives:
Why “member delegation”. The name is for the delegator: a member (the data owner) delegates a scoped slice of their own authority to a grantee. The grantee is a member too — identified by a DID, whether that’s an AI agent (as above) or the member’s own DID for a direct call (a self-grant). The same member-delegation-update / member-delegation-get contract functions (SDK: updateMemberDelegation / getMemberDelegation) cover both. This page was previously titled “Agent Auth”.
userClient here is the data owner’s own authenticated session — built the same way as t3n/agentClient above, just with the user’s own key. See Invoke your contract for the full construction and where TENANT_CONTRACT/contractVersion come from. A grant is scoped three ways at once: which contract, which functions on it, and which external hosts it may reach. An agent with no matching grant can still call the contract — the call just fails at the point it tries to reach the network, with host/http.egress_denied.

What a grant can say

Every field is snake_case on the wire, exactly as written here — camelCase keys (agentDid, scriptName, validFromSecs) are rejected rather than silently dropped, so a mistyped time-box can’t be lost.

Org delegation — the second edge

Everything above is the member edge: a data owner delegating their own authority. There is a second, independent edge — org delegation — where an organisation admin grants authority over the org’s contracts, written with the SDK’s OrgDataClient.setDelegation rather than updateMemberDelegation. A call on the delegated path needs both edges to permit it. They are enforced separately rather than as one combined check, so a missing org grant and a missing member grant fail in different places. An org grant also carries less: only functions and scopes — the qualifiers version_req, read_scopes, allowed_hosts and window are rejected on that edge, so an org grant cannot carry an expiry or a host allowlist. checkDelegation() reports which edge is missing for a given action.
Splitting authentication from authorization means a compromised or misbehaving agent key doesn’t automatically mean compromised data access — the blast radius of a leaked agent key is exactly whatever contracts, functions, and hosts a user has explicitly granted it, nothing more. It also means a user can revoke an agent’s access without the agent’s key changing at all — they just stop re-issuing the grant.

3. Stateless invocation with invoke()

Everything above assumes a session: handshake() then authenticate(), held open across calls. An org-owned agent has a second, stateless option — invoke() posts once to POST /api/invoke and authenticates by an opaque API key instead, with no handshake, no Session-Id, no session state at all:
The agent presents its key verbatim in the X-T3N-Api-Key header (t3n_key_<key-id>.<secret>) — the same key printed once when you minted the agent, see Register an Organization-owned Agent. The node derives the acting agent DID from the key per call and runs the same execution pipeline the session execute path runs, returning the contract’s decoded result directly (no JSON-RPC envelope to unwrap).
invoke() refuses to send the api key over anything but HTTPS, unless the host is a local-dev loopback (localhost / 127.0.0.1 / [::1]) — an insecure baseUrl throws before any network activity. This is a fixed transport rule, not something you configure.
Use invoke() for a one-shot call from somewhere a long-lived session doesn’t fit — a webhook handler, a serverless function, a cron job — and the session-based flow above (T3nClient.handshake()/authenticate()) for anything that makes several calls and can hold a session open. pii_did on the request is the delegated-call target — set it to act on behalf of another org member; omit it for a self call.

Direct (self) calls work the same way

If a user is invoking their own contract directly rather than through a separate agent, the same grant mechanism applies — they just grant to their own DID (a self-grant) instead of an agent’s.

Full working example

See Invoke your contract for this in context, including the search/book contract call itself. For why the outbound call fails without a grant even when the contract code is correct, see Outbound HTTP calls are authorized by the user, not the contract.
Older community code sometimes references a standalone delegation-credential API — functions for building and signing a per-call delegation credential (an “envelope”). That flow was removed when T3N moved to envelope-free authorisation, and it is not part of the current SDK (confirmed against testnet-v1.0.9, @terminal3/t3n-sdk 5.2.0). Authority is now the standing on-chain member delegation shown above (intersected with any org-granted authority), so member-delegation-update — not a per-call credential — is the write surface.