At a glance
- Three calls:
generate-intent→ wallet signsintent.payload→submit-intent, then followGET /v1/status. - Same API for every wallet: only the frontend wallet call differs, and
intent.standard(nep366,nep413,eip712orwebauthn) tells you which one. - To go live: two signatures,
agent_createthengrant_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.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.grant_issue. Every owner intent uses the same three pieces: two backend routes and one frontend call.
Backend (SDK)
Frontend
/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 anexpires_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 showpreview next to the wallet prompt. It says what the owner is agreeing to:
preview.summary: a one-line description.preview.policy: foragent_createandpolicy_update, the complete policy being signed. It is the whole resulting policy, not a diff against the current one.preview.deletion: foragent_delete, what would be lost.preview.approval: forapproval_vote, the request being voted on.
Owner identities
Theowner 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.
Passkey registration
Your app registers passkeys. The Agent API doesn’t do it for you.- Register with
pubKeyCredParamsset 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_idandorigin, and pass them as theownerinagent_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 setsowner_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:
verdictisapproveorreject.approval_idandrequest_hashidentify the approval and the exact request it decides.wallet_pubkeyis the agent wallet’s key as the approval reports it.
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).
Once the signed request is accepted:
- The API retires the agent’s wallet and archives the agent, so no new agent request is admitted.
- When the retirement is complete, the sponsor relays the owner’s signed
delete_wallet_policycall and pays the gas. - The policy’s storage deposit goes back to the sponsor, together with any native NEAR left in the wallet.
- Deposit addresses still waiting for payment, unpaid or paid in part, are closed as
FAILEDwithagent_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.
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