Partner integration
Developer Preview
Let your users set rules for their agents. Nava checks every action against those rules before it runs. See the Integrator API reference for every endpoint.
- Your user saves a policy in Nava's embedded editor.
- Your backend checks that the policy is active.
- Your server sends each transaction to Nava before your agent acts.
- Only approved transactions run.
Configure
An integration admin creates the integration in Nava's /integrations view and registers your app's origin. The API key is shown once; store it as NAVA_INTEGRATION_KEY.
The integration-mode NavaClient has no default API URL, so always pass NAVA_API_URL as baseUrl.
Keys are verify-only by default. That is enough for the ordinary embed flow below. Extra privileges (elevated write scopes, or wallet sign-up that skips the funds check) require the Nava team to allow them on the integration, then a new API key that includes those scopes.
Who can sign in
When a user opens the embedded editor, Nava signs them in inside its own popup. Your app never handles Nava passwords or tokens.
- Existing Nava users can always sign in, by email or wallet.
- New users by wallet (recommended for most partners) can create a Nava account if their wallet holds Nava's minimum native ETH on the configured chains. Ask Nava for the current amount. This works with a public,
appId-only login context — no special API key scope and no partner backend for admission. Users below the minimum see a message in the popup, and the embed surfaces anineligibleerror. - New users by email can sign up only after Nava adds each email to its allowlist. That does not suit open onboarding.
Most partners should use the wallet-with-funds path and leave advanced sign-up off.
Advanced: partner-vouched wallet sign-up
If you need to onboard wallets that may not hold the minimum yet, ask the Nava team to enable Advanced: wallet sign-up (integrator:users:signup) on your integration, then create a new integration key that includes that scope. Existing keys do not gain it.
With that key on your server only, mint a login context bound to a wallet the user has proved to you (signup: true and walletAddress). New users who complete Nava sign-in with that wallet skip the funds check. Public appId-only contexts never skip it.
Bind only a wallet the user controls in their browser. Nava's login popup signs with that wallet and refuses any other signer, so a server-held key cannot complete signup.
Mint the context on your backend and return only { id, state } to the browser:
const response = await fetch(`${NAVA_API_URL}/embed/login-contexts`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-app-id': NAVA_APP_ID,
'x-api-key': NAVA_INTEGRATION_KEY, // Server only.
},
body: JSON.stringify({
appId: NAVA_APP_ID,
parentOrigin: PARTNER_ORIGIN, // Exact origin registered on the integration.
signup: true,
walletAddress, // Wallet the user proved to your backend.
}),
});
if (!response.ok) throw new Error('Nava refused the login context.');
const { id, state } = await response.json();Pass that context into the embed with getLoginContext:
<NavaPolicyProvider
appId={NAVA_APP_ID}
baseUrl={NAVA_APP_URL}
onComplete={onPolicyComplete}
getLoginContext={async () => {
const { id, state } = await fetchSignupLoginContext(); // Your backend route.
return { id, state };
}}
>
<NavaPolicyButton />
<NavaPolicyWidget />
</NavaPolicyProvider>Requires @navalabs/sdk 0.17.2 or later.
Client-side instructions
These steps run in your app's browser. Your frontend embeds Nava's editor and sends policy-completion events and trade requests to your backend.
Embed the editor
Install the SDK (React 18 or later) and wrap your app once in NavaPolicyProvider. Everything else goes inside it.
npm install @navalabs/sdkimport {
NavaPolicyButton, NavaPolicyProvider, NavaPolicyWidget,
} from '@navalabs/sdk/react';
<NavaPolicyProvider
appId={NAVA_APP_ID}
baseUrl={NAVA_APP_URL}
onComplete={onPolicyComplete}
>
<NavaPolicyButton />
<NavaPolicyWidget />
</NavaPolicyProvider>onPolicyComplete is your frontend callback. Send the completion to a route on your backend, which runs the policy check and returns the result to your UI.
onComplete receives { appId, agentId, policyId, version, walletAddresses }. Use walletAddresses for the policy check and for trades; Nava finds the agent from the wallet (see Agent resolution).
Returning users stay signed in. The editor keeps its own Nava session inside its frame and restores it on reload, so your app never stores Nava tokens.
The editor's credentials can read and change the user's agents, wallets and policies. Nava issues them only after the user signs in and consents in Nava's popup, and binds each one to that user and your integration, so it reaches only the agents the user connected to your app. No partner API secret is involved; your browser code uses only the public NAVA_APP_ID.
| Element | What it does | Props |
|---|---|---|
NavaPolicyProvider | Holds the editor drawer. Wrap your app once. | appId, baseUrl, onComplete; optional agentId, theme (dark or light), zIndex, onOpenChange, getLoginContext |
NavaPolicyButton | Opens the editor. | Optional label (children), className, style |
NavaPolicyWidget | Shows the user's policy checks. | Optional minHeight, className, style |
NavaPolicyEditor | The editor inline, without the provider or drawer. | appId, baseUrl, onComplete; optional agentId, theme, minHeight, getLoginContext |
useNavaPolicyDrawer() | Returns open, close and isOpen. | |
useNavaPolicyWidget() | Returns refresh. |
Open the editor from your own link or button:
import { useNavaPolicyDrawer } from '@navalabs/sdk/react';
function PolicyLink() {
const { open } = useNavaPolicyDrawer();
return (
<button type="button" className="link" onClick={open}>
Set your guardrails
</button>
);
}Refresh the widget
The widget loads once. submitTrade() below is your frontend helper that calls your backend. Refresh the widget after that request returns:
const { refresh } = useNavaPolicyWidget();
await submitTrade();
refresh();Server-side instructions
These steps run on your partner backend. Keep NAVA_INTEGRATION_KEY here and use NAVA_API_URL for calls to Nava. Your frontend calls your backend; your backend calls Nava's Integrator API.
Create the Nava client
Construct the integration-mode client on your server:
const client = new NavaClient({
integration: {
appId: config.appId,
apiKey: config.integrationKey, // Private: server only.
},
baseUrl: config.apiUrl,
});Check the user has a policy
Run this check server-side, on your partner backend. When the browser receives onComplete, call your backend to read the wallet's policy using the integration-mode NavaClient. Keep NAVA_INTEGRATION_KEY on the server. Enable trading only after the backend confirms the policy is active.
Nava enforces the user's current policy on every verification.
/** True once the user has saved a policy for this wallet. */
export async function hasActivePolicy(
nava: Pick<NavaClient, 'getPolicy'>,
walletAddress: string,
) {
const policy = await nava.getPolicy({ walletAddress });
return policy.status === 'active';
}Server-side request with curl
curl "$NAVA_API_URL/integrator/policies?walletAddress=0xYourUserWallet" \
-H "x-app-id: $NAVA_APP_ID" \
-H "x-api-key: $NAVA_INTEGRATION_KEY"{
"status": "active",
"policyId": "1e957416-…",
"version": 1,
"document": { … }
}Submit a trade
Run the transaction-building and Nava verification code below on your backend when your frontend requests a trade.
/** Sends the exact swap to Nava and waits for the verdict. */
export async function verifySwap(
nava: AgentNavaClient,
prompt: string,
wallet: string,
swap: SwapInput,
idempotencyKey: string, // Reuse on retries.
) {
const { id } = await nava.requestVerification({
prompt,
proposedTx: buildSwapTransaction(wallet, swap),
escrowAddress: wallet,
idempotencyKey,
});
return { id, verdict: await nava.waitForVerification(id) };
}waitForVerification returns once Nava approves or rejects. Execute only if it is approved. To retry, send the same idempotencyKey with the same transaction; see Retries.
Server-side request with curl
curl -X POST "$NAVA_API_URL/integrator/verifications" \
-H "x-app-id: $NAVA_APP_ID" \
-H "x-api-key: $NAVA_INTEGRATION_KEY" \
-H "Idempotency-Key: swap-123" \
-H "Content-Type: application/json" \
-d '{
"escrowAddress": "0xYourUserWallet",
"prompt": "Swap 5 USDC for USDT",
"proposedTx": { "protocol": "uniswap_v3", "chainId": 1,
"to": "0x66a9…", "value": "0", "data": "0x3593…" }
}'{
"id": "ff845a3f-…",
"status": "PENDING",
"canExecute": true,
"verdict": { "outcome": "approved", "reasonCode": "allowed", … }
}If verdict is still null, poll until it is set:
curl "$NAVA_API_URL/integrator/verifications/ff845a3f-…" \
-H "x-app-id: $NAVA_APP_ID" \
-H "x-api-key: $NAVA_INTEGRATION_KEY"The response has the same shape.
Example prompts
Save a Spot swapper policy with USDC, USDT and a $100 cap, then try:
| Prompt | Result |
|---|---|
| Swap 5 USDC for USDT on Uniswap (Ethereum). | Approved |
| Swap 200 USDC for USDT on Uniswap (Ethereum). | Rejected: over the cap |
| Buy $20 of ETH on Hyperliquid testnet. | Refused: not in the policy |
Test wallets need no funds. By default nothing is signed or sent.