Native APIGet started
PreviewSet up with an agent
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.
Give this to your coding agent
Paste the line into your coding agent, or start your agent with it from your project folder.
Set up the Native API in this project by following https://docs.native.no/native-api/agent-setup.mdWorks in any coding agent that can read a web page. If yours cannot, use Copy page on the setup page and paste that in.
- It adds
NATIVE_CLIENT_IDandNATIVE_CLIENT_SECRETto 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.noorhttps://mcp.native.nountil 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, every page in one file. llms.txt lists the pages, each with a Markdown copy.
- Ask the person before you install a package the project does not already have.
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/sdkin TypeScript or JavaScript,mcpin 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.
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
# 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.exampleif there is none. - Write the empty names into a local env file too, such as
.envor.env.local, but only into one that git ignores and does not track. Check both:git check-ignore <file>prints the file, andgit ls-files <file>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.localinstead, 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.
- Add both names, with empty values, to the project’s example env file. Create
Add a token helper
#Follow Authentication: the client-credentials grant at
https://auth.native.no/oauth/token, withaudienceset tohttps://mcp.native.no/mcpandscopeset tomcp:read mcp:write. The audience stayshttps://mcp.native.no/mcpwhichever endpoint the client calls, the pinned/mcp/v1included. A token lasts at most an hour. Cache one token per audience, and fetch a new one a minute beforeexpires_inruns 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
fetchandrequests, as Authentication does. Keep the token endpoint, the audience and the MCP address as separate named constants, never repeated inline: sandbox credentials come with addresses of their own.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<string, { token: string; expiresAt: number }>() const pending = new Map<string, Promise<string>>() export async function accessToken(audience: string, scope: string): Promise<string> { 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 } }Add a client for the tools
#Put every Native API call in one server-only module, as the flow 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 instructuredContent.data.errorand raise an error that carriescodeandretryable. Branch oncode, never onmessage. A result with no envelope means the SDK refused the arguments: fix the call. See 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_statusorview_post. - Run the waiting in a background job, never inside a web request.
create_postsanswers at once with abatchId: store it, then poll from the job. A web request can time out after the batch has started, and a secondcreate_postsmakes 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_profileandget_profile(Profiles),create_connect_link(Social accounts),create_postswithget_post_batch_status(Generating posts),view_post,approve_postandreject_post(Approval), andget_post_status(Publishing). Wrapcreate_postsin two functions, as the samples do: one starts the batch and returns itsbatchId, and one waits for abatchId. 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_profileordelete_profileunless the person asks for it. These put posts in front of the public, or end or erase a client.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<Client> } | 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<Client> { 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<Client> { 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<void> { 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<T>(name: string, args: Record<string, unknown>): Promise<T> { 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<Profile>('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<string> { 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<string[]> { const started = Date.now() let delay = 5_000 let batch = await callNative<PostBatch>('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<PostBatch>('get_post_batch_status', { batchId }) } return batch.items.flatMap((item) => (item.status === 'created' && item.postId ? [item.postId] : [])) }- One MCP client per token, over Streamable HTTP at
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_workspaceswithlimit: 1, and says it worked. - On a failure, says what failed: the token request’s HTTP status, or the tool’s
code.partner_not_foundmeans Native has no partner set up for the client ID yet, so the person writes to hei@native.no. - Creates, changes and publishes nothing.
import { callNative, closeNative, NativeError } from './native-api' async function main(): Promise<number> { 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))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.
Point future sessions at the docs
#If the project has an agent instructions file, such as
AGENTS.md,CLAUDE.mdor 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 <path to the client module>.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_IDandNATIVE_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, and Native issues the credentials when it switches access on for them.
The Native API is not open yet
Partner access is being built now. Tell us what you would build, and we will be in touch when it opens.