Native API· Set up with an agent

Native APIGet started

Preview

Set 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.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.

  • 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, 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.
  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

    # 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 <file> prints the file, and git 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.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: 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 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 }
    }
  4. 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 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.
    • 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), create_connect_link (Social accounts), create_posts with get_post_batch_status (Generating posts), view_post, approve_post and reject_post (Approval), and get_post_status (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.

    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] : []))
    }
  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.
    • 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.

  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 <path to the client module>.
  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, 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.

Contact sales