The owner approves every change to their agent with their own wallet: a NEAR account, an EVM wallet or a passkey. Your backend prepares the request, your frontend has the wallet sign it, and your backend submits it.
At a glance
  • Three calls: generate-intent → wallet signs intent.payload → submit-intent, then follow GET /v1/status.
  • Same API for every wallet: only the frontend wallet call differs, and intent.standard (nep366, nep413, eip712 or webauthn) tells you which one.
  • To go live: two signatures, agent_create then grant_issue. Every later change is one signature.
  • Sign promptly: the owner has about 290 s for most intents and about 600 s for on-chain wallet requests. See Signing windows.
  • Never in the browser: your partner API key, grant tokens and the SDK stay on your backend.

Who does what

Your backend

Keeps the partner API key and grant tokens. Calls generate-intent, submit-intent and status. Saves each correlation_id with your user and agent.

Your frontend

Connects the owner’s wallet. Shows the preview, asks the wallet to sign intent.payload, and sends the result to your backend.
The browser never needs your partner API key, a grant token or the SDK. Before you create or submit an intent for a user, check that this user owns the agent. Don’t trust a correlation_id sent from a browser without that check.

The signing flow

Every owner intent works the same way.
1

Generate (your backend)

Call POST /v1/generate-intent with the intent type and its fields. The response has the correlation_id (starts with intent_), the signer whose wallet must sign, intent: { standard, payload }, a readable preview and an expires_at.
2

Sign (your frontend)

Show the preview to the owner. The wallet signs intent.payload unchanged, using the method that matches intent.standard.
3

Submit (your backend)

Call POST /v1/submit-intent with type, correlation_id and signed_data: the intent you generated plus the wallet’s output. Resubmitting the same signature is safe.
4

Wait for the result (your backend)

Call GET /v1/status?correlation_id=…&wait_ms=30000 until the status is SUCCESS or FAILED.
Here is the whole round trip for a grant_issue. Every owner intent uses the same three pieces: two backend routes and one frontend call.
Backend (SDK)
Frontend
The routes, /api/owner-intents/grant and /api/owner-intents/submit, are your app’s own. Your backend wires them to generateGrant and submitSigned, with the signed-in user’s ID from your session. Keep the Idempotency-Key stable for one logical request: a retry with the same key and body returns the same intent. Store the generated intent if signing has to survive a page reload.

What the owner signs

standard tells you which wallet call to make. Signing Standards explains each one. The wallet signs payload exactly as it is. Your frontend then sends back the intent plus the wallet’s output, called signed_data. For a NEAR owner, a policy_update can come back as either standard. Always choose the wallet call from intent.standard, never from the intent type.
Frontend
Your wallet library may name the results differently. Adjust the code to match. The API accepts common signature encodings (base58, base64 and hex). Don’t rebuild, hash or reorder the payload: the API checks that it’s exactly what it sent.

Signing windows

Every intent has an expires_at. Sign and submit before it, or you get intent_expired and have to generate a new intent. The window depends on the kind of request (Signing Standards explains the two kinds): A delegate action that a NEAR wallet signs has its own nonce and block height limit. If the owner’s key can no longer execute it by the time you submit, you get policy_delegate_expired: generate a new intent.

Show the preview

Always show preview next to the wallet prompt. It says what the owner is agreeing to:
  • preview.summary: a one-line description.
  • preview.policy: for agent_create and policy_update, the complete policy being signed. It is the whole resulting policy, not a diff against the current one.
  • preview.deletion: for agent_delete, what would be lost.
  • preview.approval: for approval_vote, the request being voted on.

Owner identities

The owner field in agent_create holds only public information. Owner Accounts shows each shape in full, and how to get an EVM public key or convert a passkey key. New agents currently take NEAR or passkey owners. With EVM owners off, agent_create for an EVM owner returns owner_type_unsupported (400) and GET /v1/network lists only near and passkey in supported_owner_types. Agents that already have an EVM owner keep working and sign as described on this page.
For EVM and passkey owners, the NEAR account comes from the public key. A different key means a different account, and keys can’t be changed later. Make sure the owner keeps access to the same key.

Passkey registration

Your app registers passkeys. The Agent API doesn’t do it for you.
  • Register with pubKeyCredParams set to ES256 (alg: -7), and require user verification.
  • Save the public key in SPKI format. If your library gives you a COSE key, convert it on your backend (see Owner Accounts).
  • Save the credential ID, public key, rp_id and origin, and pass them as the owner in agent_create.

Owner intent types

policy_update needs the complete policy and the expected_revision you read from GET /v1/agents/{agent_id}/policy. If the policy changed since, you get policy_revision_conflict: read it again and generate a new intent.

Approval votes

When the policy sets owner_approval: true, every outgoing agent action waits for the owner’s vote. The owner is the only approver, and the vote needs no grant. All owner types vote the same way through approval_vote: generate the intent, have the wallet sign intent.payload, submit. The vote is sent to the wallet provider off-chain, so it costs no gas. What gets signed is one short string:
  • verdict is approve or reject.
  • approval_id and request_hash identify the approval and the exact request it decides.
  • wallet_pubkey is the agent wallet’s key as the approval reports it.
A reject ends the request. It never ran, so the API releases its USD budget charge once it observes the rejection: the operation settles FAILED with details.never_executed. An approve releases the request to run, so the API first re-runs the checks that admitted it: the grant must still be live and unexpired, the policy applied and not frozen, and the destination still allowed by destinations. If one no longer holds, the vote is refused with authorization_stale, wallet_frozen or policy_destination_denied, nothing reaches the wallet provider, and the request stays held for a reject. An approve for a request the API has not yet recorded under its approval (its dispatch reply was lost in transport) is refused with approval_not_recorded after the API has looked once itself; send the same vote again a few minutes later. The schedule is not re-judged: the vote is the owner’s own decision. You never build this string. It is in intent.payload, and preview.approval shows the owner what is being decided. How it is signed depends on the owner:

Deleting an agent

agent_delete is one owner signature, and it’s irreversible. The owner signs the agent wallet’s delete_wallet_policy call, so it is a wallet request like agent_create and the freeze intents, not an off-chain consent message: The agent doesn’t need to be frozen or archived first. The signing window is the wallet-request window, 600 s (see Signing windows).
Nothing is swept before deletion. Public and confidential balances still in the wallet are destroyed. Before the owner signs, show preview.deletion: assets_lost, the public and confidential balances, and native_balance. Have the owner withdraw all funds first, for example by asking the agent to do it. The preview can go stale, so read it just before signing. A submission is refused with deletion_assets_remaining while the wallet holds any public or confidential balance, whatever the preview showed. The API reads the balances again right before it commits: funds that arrived since fail the deletion with deletion_assets_remaining, and a balance it cannot read with provider_unavailable. Both are recorded as failed, nothing is retired and the agent stays archived and restorable. A signature from a deletion that was later restored is refused with owner_command_stale: sign again. Funds at the agent’s EVM or Solana address aren’t swept either: move them first. See Native Accounts.
Once the signed request is accepted:
  1. The API retires the agent’s wallet and archives the agent, so no new agent request is admitted.
  2. When the retirement is complete, the sponsor relays the owner’s signed delete_wallet_policy call and pays the gas.
  3. The policy’s storage deposit goes back to the sponsor, together with any native NEAR left in the wallet.
  4. Deposit addresses still waiting for payment, unpaid or paid in part, are closed as FAILED with agent_deleted. They never hold up the deletion, but anything sent to one afterwards lands in the retired account and is lost, so tell payers to stop first.
Step 3 is best-effort and the deletion stands either way. GET /v1/status reports it in details.policy_record: Keep following the status while policy_record.status is pending. A record that stays kept still refunds the same account whenever it is deleted later. If the signed request doesn’t delete exactly this agent’s wallet policy, you get wallet_request_mismatch (409). If a NEAR delegate can no longer be executed by the owner’s key, you get policy_delegate_expired (409), nothing is retired and nothing changes: generate a new intent.

Errors

Which key error you get

The owner’s NEAR key must be a full-access key on the owner’s account. Which code a wrong or weak key produces depends on the flow:

Not supported

  • Changing the owner’s key
  • Recovering a lost wallet
  • Moving an agent to a different owner
Protocol references: NEP-366, NEP-413, NEP-616, EIP-712.