# Set up with an agent Source: https://docs.native.no/native-api/agent-setup > **Preview.** The Native API is not live yet. This page describes the planned interface; names and fields can change before access opens. Give your coding agent one line, and it adds the Native API to your project: the credentials, a token helper, an MCP client and a check you can run. > **Not live for partners yet.** Partners cannot call the Native API yet. Partner sign-in with client credentials is still in development, and once it ships it stays off until Native switches it on for you. Your agent can write the code today, so it is ready when your access opens. The line to give your coding agent: ```text Set up the Native API in this project by following https://docs.native.no/native-api/agent-setup.md ``` Works in any coding agent that can read a web page. If yours cannot, use Copy page on the setup page and paste that in. Or start your agent with it, from your project folder: Claude Code: Claude Code asks before it reads docs.native.no. ```bash claude 'Set up the Native API in this project by following https://docs.native.no/native-api/agent-setup.md' ``` Codex: By default Codex cannot fetch a new page. Approve its network request when it asks, start it with `codex --search`, or use Copy page on the setup page and paste that in. ```bash codex 'Set up the Native API in this project by following https://docs.native.no/native-api/agent-setup.md' ``` Cursor CLI: Cursor asks before it reads docs.native.no. ```bash agent 'Set up the Native API in this project by following https://docs.native.no/native-api/agent-setup.md' ``` Copilot CLI: Copilot asks before it reads docs.native.no. ```bash copilot -i 'Set up the Native API in this project by following https://docs.native.no/native-api/agent-setup.md' ``` Gemini CLI: Gemini CLI asks before it reads docs.native.no. ```bash gemini -i 'Set up the Native API in this project by following https://docs.native.no/native-api/agent-setup.md' ``` [Open in Cursor](https://cursor.com/link/prompt?text=Set%20up%20the%20Native%20API%20in%20this%20project%20by%20following%20https%3A%2F%2Fdocs.native.no%2Fnative-api%2Fagent-setup.md): opens Cursor’s chat with the line filled in, without sending it. - It adds `NATIVE_CLIENT_ID` and `NATIVE_CLIENT_SECRET` to your example env file, empty. You fill them in when Native issues your credentials. - It adds a token helper, a small client for the tools and a check you can run. - It never writes a secret into a file git tracks, and never puts one in browser or mobile code. - It does not call the Native API, since partner access is not open yet. - It adds nothing that schedules, publishes, deactivates or deletes, unless you ask for it. - It asks before it installs a package, and tells you what it changed. ## Instructions for your agent If the person asked you to set up the Native API, follow these steps in order, and keep to these rules throughout: - The Native API is not live for partners yet. Write the code, but make no call to `https://auth.native.no` or `https://mcp.native.no` until the person’s access opens. - Use only what these docs state: the tool names, fields, scopes and addresses on these pages. When you need something they do not cover, leave a `TODO(native-api)` comment saying what is missing, tell the person, and do not guess. - For more detail, read [llms-full.txt](https://docs.native.no/llms-full.txt), every page in one file. [llms.txt](https://docs.native.no/llms.txt) lists the pages, each with a Markdown copy. - Ask the person before you install a package the project does not already have. ## 1. Look at the project first Before you write anything, find out the language and framework, the package manager, where server-side code lives, where environment config lives, and which HTTP client and MCP SDK the project already uses. Write the code in the project’s own language, style and folders. If it already has Native code, for example from the Norn API setup, build on that rather than adding a second copy. The Native API is MCP tools, not REST endpoints. Call them with the official MCP SDK: `@modelcontextprotocol/sdk` in TypeScript or JavaScript, `mcp` in Python. These docs show MCP calls in TypeScript, so check the Python names below against the version the project installs. For another language, use its official MCP SDK, or leave a TODO if it has none. If the project has no server side, such as a browser app, a mobile app or a static site, stop here and tell the person. The client secret must never reach a browser or a mobile app, so the calls need a backend to run from. In a repository with several apps, use the one with a server side, and say which you chose. ## 2. Add the credentials The Native API and the Norn API share one login: a client ID and secret that Native issues when access opens. Use exactly these names: .env.example: ```bash # Native login, shared by the Native API and the Norn API (https://docs.native.no/authentication). # Native issues these when your access opens. Never commit real values. NATIVE_CLIENT_ID= NATIVE_CLIENT_SECRET= ``` - Add both names, with empty values, to the project’s example env file. Create `.env.example` if there is none. - Write the empty names into a local env file too, such as `.env` or `.env.local`, but only into one that git ignores and does not track. Check both: `git check-ignore ` prints the file, and `git ls-files ` prints nothing. Never write a real value into a file git tracks. - If the local env file is tracked, do not write there, and do not add it to `.gitignore`: that does not stop git tracking it. Use an ignored file such as `.env.local` instead, and name it in your summary. - If the project checks its environment in code, add both as optional and server-only, so the app still starts while they are empty. - If the names are already there, leave them as they are. ## 3. Add a token helper Follow [Authentication](https://docs.native.no/authentication#get-a-token): the client-credentials grant at `https://auth.native.no/oauth/token`, with `audience` set to `https://mcp.native.no/mcp` and `scope` set to `mcp:read mcp:write`. The audience stays `https://mcp.native.no/mcp` whichever endpoint the client calls, the pinned `/mcp/v1` included. A token lasts at most an hour. Cache one token per audience, and fetch a new one a minute before `expires_in` runs out. Calls that arrive while a token is on its way share that request, so a busy backend fetches one token, not one per call. Put it in a server-only module. If the project already has this helper, reuse it. Use the HTTP client the project has: the samples use `fetch` and `requests`, as Authentication does. Keep the token endpoint, the audience and the MCP address as separate named constants, never repeated inline: [sandbox credentials](https://docs.native.no/authentication#sandbox) come with addresses of their own. TypeScript: ```ts const TOKEN_URL = 'https://auth.native.no/oauth/token' // Cache one token per API and fetch a new one a minute before it runs out. // Calls that arrive while a token is on its way share that request. const cache = new Map() const pending = new Map>() export async function accessToken(audience: string, scope: string): Promise { const key = `${audience} ${scope}` const hit = cache.get(key) if (hit && hit.expiresAt - Date.now() > 60_000) return hit.token let request = pending.get(key) if (!request) { request = requestToken(audience, scope) .then((entry) => { cache.set(key, entry) return entry.token }) .finally(() => pending.delete(key)) pending.set(key, request) } return request } async function requestToken(audience: string, scope: string) { const res = await fetch(TOKEN_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ grant_type: 'client_credentials', client_id: process.env.NATIVE_CLIENT_ID, client_secret: process.env.NATIVE_CLIENT_SECRET, audience, scope, }), }) if (!res.ok) throw new Error(`Token request failed: ${res.status}`) const body = (await res.json()) as { access_token: string; expires_in: number } return { token: body.access_token, expiresAt: Date.now() + body.expires_in * 1000 } } ``` Python: ```python import os import time import requests TOKEN_URL = "https://auth.native.no/oauth/token" # Cache one token per API and fetch a new one a minute before it runs out. _cache: dict[str, tuple[str, float]] = {} def access_token(audience: str, scope: str) -> str: key = f"{audience} {scope}" hit = _cache.get(key) if hit and hit[1] - time.time() > 60: return hit[0] res = requests.post( TOKEN_URL, data={ "grant_type": "client_credentials", "client_id": os.environ["NATIVE_CLIENT_ID"], "client_secret": os.environ["NATIVE_CLIENT_SECRET"], "audience": audience, "scope": scope, }, timeout=30, ) res.raise_for_status() body = res.json() _cache[key] = (body["access_token"], time.time() + body["expires_in"]) return body["access_token"] ``` ## 4. Add a client for the tools Put every Native API call in one server-only module, as [the flow](https://docs.native.no/native-api/flow#connect-to-the-gateway) does: - One MCP client per token, over Streamable HTTP at `https://mcp.native.no/mcp`, with the token as a bearer token on the transport. Make a new client when the token changes, and keep it for every call until then rather than connecting for each call. Cache the client while it connects, so calls that arrive together share one, and drop it if connecting fails. - A request timeout of 150 seconds on every call. The gateway waits up to 120 seconds for Native, and the SDK’s default is shorter. - On `isError`, read the envelope in `structuredContent.data.error` and raise an error that carries `code` and `retryable`. Branch on `code`, never on `message`. A result with no envelope means the SDK refused the arguments: fix the call. See [Errors](https://docs.native.no/native-api/errors). - Never resend a write after a timeout. A repeated call is a new attempt, so read the state first with `get_post_status`, `get_post_batch_status` or `view_post`. - Run the waiting in a background job, never inside a web request. `create_posts` answers at once with a `batchId`: store it, then poll from the job. A web request can time out after the batch has started, and a second `create_posts` makes a second batch. Then add typed wrappers, with the fields on each tool’s page, for the tools that onboard a client and get its posts approved: `create_profile` and `get_profile` ([Profiles](https://docs.native.no/native-api/profiles)), `create_connect_link` ([Social accounts](https://docs.native.no/native-api/social-accounts)), `create_posts` with `get_post_batch_status` ([Generating posts](https://docs.native.no/native-api/generating-posts)), `view_post`, `approve_post` and `reject_post` ([Approval](https://docs.native.no/native-api/approval)), and `get_post_status` ([Publishing](https://docs.native.no/native-api/publishing)). Wrap `create_posts` in two functions, as the samples do: one starts the batch and returns its `batchId`, and one waits for a `batchId`. Poll a batch as the flow does, and give up after 15 minutes. Add no wrapper for `schedule_post`, `reschedule_posts`, `publish_post_now`, `unschedule_platform`, `deactivate_profile` or `delete_profile` unless the person asks for it. These put posts in front of the public, or end or erase a client. TypeScript: ```ts import { Client } from '@modelcontextprotocol/sdk/client/index.js' import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js' import { accessToken } from './native-token' const MCP_URL = 'https://mcp.native.no/mcp' // The token's audience stays the same whichever endpoint MCP_URL names. const NATIVE_API_AUDIENCE = 'https://mcp.native.no/mcp' let cached: { token: string; client: Promise } | undefined // One client per token: the token is set on the transport, so a new token needs a new client. // The client is cached while it connects, so calls that arrive together share it. async function nativeClient(): Promise { const token = await accessToken(NATIVE_API_AUDIENCE, 'mcp:read mcp:write') if (cached?.token === token) return cached.client const client = connect(token) cached = { token, client } // A connection that fails is dropped, so the next call tries again. client.catch(() => { if (cached?.client === client) cached = undefined }) return client } async function connect(token: string): Promise { const client = new Client({ name: 'partner-backend', version: '1.0.0' }) await client.connect( new StreamableHTTPClientTransport(new URL(MCP_URL), { requestInit: { headers: { Authorization: `Bearer ${token}` } }, }), ) return client } export async function closeNative(): Promise { const connecting = cached?.client cached = undefined // A client that never connected has nothing to close. const client = await connecting?.catch(() => undefined) await client?.close() } export class NativeError extends Error { code: string retryable: boolean body: object constructor(code: string, retryable: boolean, body: object) { super(code) this.code = code this.retryable = retryable this.body = body } } // Every call goes through here, with a timeout above the gateway's 120 seconds. export async function callNative(name: string, args: Record): Promise { const client = await nativeClient() const result = await client.callTool({ name, arguments: args }, undefined, { timeout: 150_000 }) const data = (result.structuredContent as { data?: any } | undefined)?.data if (!result.isError) return data as T // No envelope: the SDK refused the arguments before the tool ran. Fix the call. if (!data?.error) throw new Error(`${name}: arguments refused by the input schema`) throw new NativeError(data.error.code, data.error.retryable, data.error) } // Wrappers, with the fields on each tool's page. Add the others the same way. type BuildStatus = 'building' | 'ready' | 'failed' export interface Profile { brandId: string brandName: string companyId: string isActive: boolean readiness: { canGenerate: boolean; canPublish: boolean; blockers: string[] } onboarding?: { status: BuildStatus; error?: string } // in development } export const getProfile = (brandId: string) => callNative('get_profile', { brandId }) export const createProfile = (input: { websiteUrl: string; name: string; externalId: string }) => callNative<{ brandId: string; companyId: string; status: BuildStatus; created: boolean }>( 'create_profile', input, ) interface PostBatch { settled: boolean imagesPendingCount?: number items: { status: string; postId: string | null }[] } // Starts a batch from one brief and answers at once. Store the batchId before you wait. export async function createPosts(brandId: string, brief: string, count: number): Promise { const { batchId } = await callNative<{ batchId: string }>('create_posts', { brandId, brief, count }) return batchId } // Waits for a batch, polling as the flow page does, and returns the created post ids. // Run it in a background job, never inside a web request. export async function waitForBatch(batchId: string): Promise { const started = Date.now() let delay = 5_000 let batch = await callNative('get_post_batch_status', { batchId }) while (!batch.settled || (batch.imagesPendingCount ?? 0) > 0) { const elapsed = Date.now() - started if (elapsed > 15 * 60_000) throw new Error(`Batch ${batchId} has not finished after 15 minutes`) // 5 seconds for the first minute, then 15, growing to no more than 30. if (elapsed > 60_000) delay = Math.min(30_000, Math.max(15_000, delay * 1.5)) await new Promise((resolve) => setTimeout(resolve, delay)) batch = await callNative('get_post_batch_status', { batchId }) } return batch.items.flatMap((item) => (item.status === 'created' && item.postId ? [item.postId] : [])) } ``` Python: ```python import asyncio import time from contextlib import AsyncExitStack from datetime import timedelta from typing import Any from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client from native_token import access_token MCP_URL = "https://mcp.native.no/mcp" # The token's audience stays the same whichever endpoint MCP_URL names. NATIVE_API_AUDIENCE = "https://mcp.native.no/mcp" TIMEOUT = timedelta(seconds=150) # above the gateway's 120 seconds class NativeError(Exception): def __init__(self, code: str, retryable: bool, body: dict[str, Any]) -> None: super().__init__(code) self.code, self.retryable, self.body = code, retryable, body class Native: """One MCP session per token. Use it from one task: async with Native() as native.""" def __init__(self) -> None: self._stack: AsyncExitStack | None = None self._session: ClientSession | None = None self._token: str | None = None async def __aenter__(self) -> "Native": return self async def __aexit__(self, *exc: object) -> None: await self._close() async def _close(self) -> None: if self._stack: await self._stack.aclose() self._stack, self._session, self._token = None, None, None async def _connect(self) -> ClientSession: token = await asyncio.to_thread(access_token, NATIVE_API_AUDIENCE, "mcp:read mcp:write") if self._session and token == self._token: return self._session # The token is set on the transport, so a new token needs a new session. await self._close() stack = AsyncExitStack() try: read, write, _ = await stack.enter_async_context( streamablehttp_client(MCP_URL, headers={"Authorization": f"Bearer {token}"}) ) session = await stack.enter_async_context(ClientSession(read, write)) await session.initialize() except BaseException: # A refused token fails here: close what was opened, then raise. await stack.aclose() raise self._stack, self._session, self._token = stack, session, token return session async def call(self, name: str, args: dict[str, Any]) -> Any: session = await self._connect() result = await session.call_tool(name, args, read_timeout_seconds=TIMEOUT) data = (result.structuredContent or {}).get("data") if not result.isError: return data # No envelope: the SDK refused the arguments before the tool ran. Fix the call. if not isinstance(data, dict) or "error" not in data: raise RuntimeError(f"{name}: arguments refused by the input schema") error = data["error"] raise NativeError(error["code"], error["retryable"], error) # Wrappers, with the fields on each tool's page. Add the others the same way. async def get_profile(self, brand_id: str) -> dict[str, Any]: return await self.call("get_profile", {"brandId": brand_id}) async def create_profile(self, website_url: str, name: str, external_id: str) -> dict[str, Any]: args = {"websiteUrl": website_url, "name": name, "externalId": external_id} return await self.call("create_profile", args) async def create_posts(self, brand_id: str, brief: str, count: int) -> str: """Starts a batch from one brief and answers at once. Store the batch id before you wait.""" args = {"brandId": brand_id, "brief": brief, "count": count} return (await self.call("create_posts", args))["batchId"] async def wait_for_batch(self, batch_id: str) -> list[str]: """Waits for a batch, polling as the flow page does, and returns the created post ids. Run it in a background job, never inside a web request. """ started, delay = time.monotonic(), 5.0 batch = await self.call("get_post_batch_status", {"batchId": batch_id}) while not batch["settled"] or (batch.get("imagesPendingCount") or 0) > 0: elapsed = time.monotonic() - started if elapsed > 15 * 60: raise TimeoutError(f"Batch {batch_id} has not finished after 15 minutes") # 5 seconds for the first minute, then 15, growing to no more than 30. if elapsed > 60: delay = min(30.0, max(15.0, delay * 1.5)) await asyncio.sleep(delay) batch = await self.call("get_post_batch_status", {"batchId": batch_id}) return [item["postId"] for item in batch["items"] if item["status"] == "created"] ``` ## 5. Add a check the person can run Add a script the person runs once their credentials are in, wired to the way the project runs scripts. Load the env file the way the app does, so the check reads the same values. The check: - Names each missing variable plainly and stops, before any network call. - Otherwise fetches a token and makes one read-only call, `get_workspaces` with `limit: 1`, and says it worked. - On a failure, says what failed: the token request’s HTTP status, or the tool’s `code`. `partner_not_found` means Native has no partner set up for the client ID yet, so the person writes to [hei@native.no](mailto:hei@native.no). - Creates, changes and publishes nothing. TypeScript: ```ts import { callNative, closeNative, NativeError } from './native-api' async function main(): Promise { const missing = ['NATIVE_CLIENT_ID', 'NATIVE_CLIENT_SECRET'].filter((name) => !process.env[name]) if (missing.length > 0) { console.log(`Native API: ${missing.join(' and ')} not set. Native issues them when your access opens.`) return 1 } try { // Read only: lists the workspaces the token can reach, and changes nothing. const { totalCount } = await callNative<{ totalCount: number }>('get_workspaces', { limit: 1 }) console.log(`Native API: signed in. The token reaches ${totalCount} workspace(s).`) return 0 } catch (err) { if (err instanceof NativeError && err.code === 'partner_not_found') { console.error('Native API: Native has no partner set up for this client ID. Write to hei@native.no.') } else if (err instanceof NativeError) { console.error(`Native API refused the call: ${err.code}`) } else { console.error(`Native API check failed: ${(err as Error).message}`) } return 1 } finally { await closeNative() } } main().then((code) => process.exit(code)) ``` Python: ```python import asyncio import os import sys from native_api import Native, NativeError async def main() -> int: missing = [n for n in ("NATIVE_CLIENT_ID", "NATIVE_CLIENT_SECRET") if not os.environ.get(n)] if missing: print(f"Native API: {' and '.join(missing)} not set. Native issues them when your access opens.") return 1 try: async with Native() as native: # Read only: lists the workspaces the token can reach, and changes nothing. result = await native.call("get_workspaces", {"limit": 1}) except NativeError as err: if err.code == "partner_not_found": print("Native API: Native has no partner set up for this client ID. Write to hei@native.no.") else: print(f"Native API refused the call: {err.code}") return 1 except Exception as err: print(f"Native API check failed: {err}") return 1 print(f"Native API: signed in. The token reaches {result['totalCount']} workspace(s).") return 0 if __name__ == "__main__": sys.exit(asyncio.run(main())) ``` If you can run commands here and the variables are still empty, run it once: it must name both and stop before any network call. Leave the first real run to the person. ## 6. Point future sessions at the docs If the project has an agent instructions file, such as `AGENTS.md`, `CLAUDE.md` or a rule file in `.cursor/rules/`, add one line to it, with the real path. In `.cursor/rules/`, add it to an existing general rule, or skip this step when none fits. Do not create such a file if there is none. ```text - Native API: docs for coding agents at https://docs.native.no/llms.txt. Our client is in . ``` ## 7. Tell the person what changed Finish with a short summary for the person: - The files you added or changed, and any package you installed with their approval. - The two variables to fill in, `NATIVE_CLIENT_ID` and `NATIVE_CLIENT_SECRET`, and the file they go in. - How to run the check. - Every TODO you left, and why. - How access opens. Partner access to the Native API is not open yet: the person asks for it through [Contact sales](https://native.no/contact-sales), and Native issues the credentials when it switches access on for them.