# From onboarding to offboarding Source: https://docs.native.no/native-api/flow > **Preview.** The Native API is not live yet. This page describes the planned interface; names and fields can change before access opens. The calls a partner backend makes for one client, in order: onboard, set the look, connect accounts, generate, approve, publish, track and offboard. > **Not open to partners yet.** Partner access is not live. Sign-in with client credentials is still in development, so no partner backend can call these tools yet. Tools marked Preview run in the gateway today for people signed in to Native. Tools and fields marked In development are not deployed yet. Names and fields can still change before access opens. A partner runs Native for its own clients from its backend. Each client is its own Native workspace with one brand. Every call names the client by its `brandId`, or by the id of one of its posts. This page follows one client from the first call to the last. Each step names its tools and shows a short example. Every field and error is on the tool pages: [Profiles](https://docs.native.no/native-api/profiles), [Branding](https://docs.native.no/native-api/branding), [Social accounts](https://docs.native.no/native-api/social-accounts), [Generating posts](https://docs.native.no/native-api/generating-posts), [Editing posts](https://docs.native.no/native-api/editing-posts), [Approval](https://docs.native.no/native-api/approval) and [Publishing](https://docs.native.no/native-api/publishing). ## Connect to the gateway The tools are served over MCP Streamable HTTP at `https://mcp.native.no/mcp`. Use the official MCP SDK rather than writing JSON-RPC by hand. The gateway keeps no session: each request stands on its own. Your backend signs in with an Auth0 client-credentials token. [Authentication](https://docs.native.no/authentication) covers how to get one, and its token helper, `accessToken`, is what the code below imports. Two things shape the code below: - A token lasts an hour at most. Cache it, and fetch a new one shortly before `expires_in` runs out. - The token is set when the transport is made. Make a new client with each new token. TypeScript: ```ts import { Client } from '@modelcontextprotocol/sdk/client/index.js' import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js' // The token helper from Authentication. 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 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 // 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) } ``` Raise the timeout as shown. Most calls return within seconds, but a write that contacts a platform, such as `schedule_post` or `publish_post_now`, can take much longer. The gateway waits up to 120 seconds for Native before it answers `upstream_timeout`. The SDK’s default timeout is shorter, so without the raise your side can give up on a call that has already succeeded on ours. A failed call comes back with `isError` and an envelope holding `code`, `message` and `retryable`. Branch on `code`: Native writes `message` for people and can change it. `retryable` says whether the same call, unchanged, could succeed. [Errors](https://docs.native.no/native-api/errors) lists every code. > **A pinned version is in development.** A versioned endpoint, `https://mcp.native.no/mcp/v1`, is in development. Once it ships, partners are meant to call it instead of `/mcp`. It takes the same token, so only the URL changes. ## The steps The steps run in this order for each client. One can start early: the client can connect accounts while Native builds the profile. The examples use `callNative` from [the code above](https://docs.native.no/native-api/flow#connect-to-the-gateway). ## 1. Onboard a client MCP tool `create_profile` (in development) Send the client’s website, its business name and your own id for the client. `create_profile` makes a workspace of its own for the client, on your plan, with no browser and no checkout. Arguments: ```json { "websiteUrl": "https://pipedream.example", "name": "Pipe Dream Plumbing", "externalId": "client-1042" } ``` Native answers at once with the `brandId` that every other tool takes. It builds the profile in the background: it reads the website and fills in the business profile, the brand’s look, its knowledge, its products and its writing style, as it does for a customer who signs up in the app. Result: ```json { "brandId": "1f461fa4-a41c-4efd-81ba-2946ca61d479", "companyId": "c51c8e77-4123-4461-a185-c5e8475174a9", "status": "building", "created": true } ``` `externalId` makes the call safe to repeat. A second call with the same `externalId` answers with the profile the first call made, with `created: false`, whatever else it sends. `allowTeamPhotos` is off by default. Leave it off unless the client has agreed. It lets Native keep pictures of the client’s staff from the website and recognise them in pictures you upload. Send the client’s own website, not a social media profile. A social media address is refused with `invalid_website_url`, and nothing is created. ## 2. Wait until the profile is ready MCP tool `get_profile` (preview) A build typically takes 10 to 30 minutes. Poll `get_profile` with the `brandId` about once a minute, until `onboarding.status` is `ready` or `failed`. The `onboarding` field is in development. It appears only on a profile that `create_profile` made. Result while the profile builds, shortened: ```json { "brandId": "1f461fa4-a41c-4efd-81ba-2946ca61d479", "brandName": "Pipe Dream Plumbing", "isActive": true, "channels": [], "readiness": { "canGenerate": false, "canPublish": false, "blockers": [ "profile_not_ready", "no_channel_connected" ] }, "onboarding": { "status": "building" } } ``` Until the build is `ready`, the tools that generate refuse the profile with `profile_not_ready`: `create_posts`, `generate_caption`, `generate_image`, `regenerate_post_image`, ad generation, and `create_draft` when it is sent a `prompt` or `includeImage: true`. That includes a profile whose build has failed. A `create_draft` with your own caption is not refused. When a build fails, `onboarding.error` says why: `website_unreachable`, `analysis_failed`, `enrichment_failed`, `build_not_queued` or `build_interrupted`. Treat the list as open, since more codes can come. To retry, call `create_profile` again with the same `externalId`. The build restarts where it stopped. ## 3. Set the brand’s look MCP tool `get_brand_style` (in development) MCP tool `update_brand_style` (in development) MCP tool `upload_brand_logo` (in development) The build takes the brand’s colours and fonts from the website. Once the profile is ready, read what it found with `get_brand_style`, logos included, and correct what is wrong. `update_brand_style` adds palette colours, sets the colour roles and sets the fonts. Pass only what changes: everything you leave out stays as it is. A font must be in the brand’s library or be a Google Fonts family. Otherwise the call fails with `font_not_found`, and no role changes. Arguments: ```json { "brandId": "1f461fa4-a41c-4efd-81ba-2946ca61d479", "colors": { "background": "#F7F4EE", "accent": "#1F5FAD", "text": "#1B1D21", "secondary": "auto" }, "fonts": { "heading": "Fraunces", "body": "Inter" } } ``` `upload_brand_logo` saves a logo from a public https URL or as base64. By default it also makes it the logo Native uses in images and ads. The logo it replaces stays among the brand’s logos. > **Set the look before you generate.** A style change never redraws a post that already exists, approved and scheduled posts included. Only posts made after the change carry the new look. ## 4. Connect the client’s social accounts MCP tool `create_connect_link` (preview) MCP tool `get_connect_options` (in development) MCP tool `complete_connect` (in development) `create_connect_link` mints a single-use link that the client opens to connect an account. It expires about 10 minutes after you mint it. Hand it to the client, and do not load it yourself: loading it uses it up. Today the link opens Native’s connect page. Two parameters are in development. `platform` makes the link headless: it goes straight to the platform’s own login and consent screen. A headless link needs `returnUrl`, an https URL of yours. When the client is done, the browser comes back to it. Arguments for a headless link: ```json { "brandId": "1f461fa4-a41c-4efd-81ba-2946ca61d479", "platform": "instagram", "returnUrl": "https://app.partner.example/native/connected?client=client-1042" } ``` If Native cannot make a headless link for the brand, the call fails with `headless_connect_unavailable`. Call it again without `platform` and `returnUrl` for a link to Native’s connect page. Native adds `status` to your `returnUrl`, and replaces any `status`, `platform`, `connectToken` or `error` already on it: | `status` | Also on the URL | What to do | | --- | --- | --- | | `connected` | `platform` | Confirm with `get_profile`. | | `selection_required` | `platform`, `connectToken` | The client manages several Facebook Pages, LinkedIn organisations, Pinterest boards or Google Business locations. Pass `connectToken` to `get_connect_options`, let the client choose, then call `complete_connect` with their choice. | | `failed` | `platform`, `error` | `error` is a code such as `oauth_denied`, `link_expired` or `account_in_another_workspace`. Mint a fresh link to retry. | `complete_connect` replaces any account the brand already had on that platform. Call it only with the choice the client made. The `connectToken` lasts about 10 minutes. After that, both tools fail with `connect_session_expired`, and the client needs a fresh link. Then confirm the connection. Call `get_profile` once, then every 10 seconds for up to 2 minutes, until the channel’s `health` is `connected` and `readiness.canPublish` is `true`. A `failed` status needs a fresh link, not more polling. ## 5. Generate posts MCP tool `create_posts` (preview) MCP tool `get_post_batch_status` (preview) `create_posts` turns one brief into 1 to 20 draft posts. Each takes a different angle and gets copy written for each destination. Leave out `platforms` to use the brand’s defaults. Destinations here are uppercase names such as `INSTAGRAM`. `get_profile` and `create_connect_link` use lowercase names such as `instagram`. Arguments: ```json { "brandId": "1f461fa4-a41c-4efd-81ba-2946ca61d479", "brief": "Frozen pipes: how to prevent them this winter, and that we come out the same day, seven days a week.", "count": 3, "platforms": [ "INSTAGRAM", "FACEBOOK" ] } ``` It answers at once with a `batchId`, and the posts are written in the background. Poll `get_post_batch_status` every 5 seconds for the first minute, then every 15 seconds, growing to no more than 30. The batch is finished when `settled` is `true` and `imagesPendingCount` is 0. `settled` alone covers the copy: pictures can still be rendering. TypeScript: ```ts const { batchId } = await callNative('create_posts', { brandId, brief, count: 3 }) 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 }) } const postIds = batch.items .filter((item: { status: string }) => item.status === 'created') .map((item: { postId: string }) => item.postId) ``` A batch can partly fail. Each item has its own `status`, and a failed item carries an `error` with a `code`. If a batch has not finished after about 15 minutes, stop and raise an alert. Created items will carry their destinations, `imageUrls`, `contentDigest` and `approval` (in development), so you can show and approve a post without reading it again. Until then, read each created post with `view_post`, passing its `postId` as `id`. `scheduledAt` on `create_posts` only records an intended time. It does not schedule anything. Generation spends the Native tokens on your plan. When they run out, the call fails with `usage_limit_exceeded`, and `resetDate` says when they refill. ## 6. Edit posts MCP tool `update_post` (preview) MCP tool `generate_caption` (preview) MCP tool `regenerate_post_image` (preview) MCP tool `get_generation_job` (preview) `update_post` overwrites the caption, the first comment or the destinations. Set `platform` to change one destination’s caption or first comment only. No history is kept: when the call overwrites something, `replaced` in the result holds the previous values. `generate_caption` rewrites a caption in the brand’s voice and replaces the current one. `instructions` steers the rewrite. `regenerate_post_image` redraws one picture and leaves the other slides alone. It answers at once with `imageGeneration.jobId`. Poll `get_generation_job` with it until the job is `completed` or `failed`. TypeScript: ```ts const { imageGeneration } = await callNative('regenerate_post_image', { postId, index: 0, prompt: 'The same van, parked outside a house in fresh snow', }) let job do { // Each call waits up to about 24 seconds for the render to settle. job = await callNative('get_generation_job', { jobId: imageGeneration.jobId }) } while (job.status !== 'completed' && job.status !== 'failed') ``` Do not call `regenerate_post_image` again to retry a render: poll the job you have. While a render is running, a second call is refused with `image_generation_in_progress`, which carries the running render’s `jobId`. Once it has ended, a second call starts a new render. Template rendering is in development, and partner workspaces will have it on by default. On a workspace that renders posts from design templates, `regenerate_post_image` will be refused with `template_rendering`. Use `generate_image` there: it redraws the post’s whole design, replaces all its pictures and keeps the slide text exact. These pages do not cover `generate_image` yet: `tools/list` has its schema. A template render takes a few minutes. Every change to the content changes the post’s `contentDigest`, so an approval given before the change no longer holds: scheduling and publishing refuse the post with `approval_stale`. After editing, read the post again with `view_post` and have that version reviewed. A published post cannot be edited: edits are refused with `post_already_published`. ## 7. Get the client’s approval MCP tool `view_post` (preview) MCP tool `approve_post` (preview) MCP tool `reject_post` (preview) Show the client the post as `view_post` returns it. Each entry in `destinations` holds the caption, first comment, title, pictures and publishing options that will publish on that channel. Keep the `contentDigest` from that same read. When the client approves, call `approve_post` with that digest as `expectedDigest`. Name the person who approved in `reviewerName` and `reviewerEmail`. Without them, Native records the caller as the reviewer. TypeScript: ```ts const post = await callNative('view_post', { id: postId }) // Show post.destinations to the client. When they approve: try { await callNative('approve_post', { id: postId, expectedDigest: post.contentDigest, reviewerName: 'Dana Brooks', reviewerEmail: 'dana@pipedream.example', }) } catch (err) { if (!(err instanceof NativeError) || err.code !== 'post_content_changed') throw err // The post changed after the client saw it. Read it again and show the new version. } ``` If the post changed after your read, the approval is refused with `post_content_changed`. Read the post again and show the client the new version. Never resend with a newer digest the client did not see. When the client turns a post down, call `reject_post` with their `reason`. While the latest decision is a rejection, scheduling and publishing refuse the post with `approval_rejected`, even after edits. Only a new `approve_post` clears it. By default, partner workspaces will require an approval before anything is scheduled or published (in development). A post with no decision on record will be refused with `approval_required`. The digest does not cover the scheduled time. Moving an approved post to another time does not need a new approval. One limit to know: on a video post, the digest covers the subtitle files, but the API cannot show subtitles yet. A subtitle change makes an approval stale with no change your reviewer can see. ## 8. Schedule or publish MCP tool `schedule_post` (preview) MCP tool `reschedule_posts` (preview) MCP tool `publish_post_now` (preview) MCP tool `unschedule_platform` (preview) `schedule_post` books a post, or some of its destinations, for a time. It also moves a post that is already booked, and the post stays booked throughout. `reschedule_posts` books or moves up to 25 posts in one call. `publish_post_now` publishes at once. Arguments: ```json { "id": "faa2595d-41e8-4645-8332-4f4f9df9e9b8", "scheduledAt": "2026-11-02T09:00:00-05:00" } ``` Times are ISO 8601, with `Z` or an offset. The gateway converts them to UTC. Report a post as scheduled only when `scheduleConfirmed` in the result is `true`. Before it contacts any platform, `schedule_post` checks each destination. If one fails, for example with a caption that is too long or a picture outside Instagram’s aspect band, the call fails with `schedule_preflight_failed`. Nothing is booked, and `blockedDestinations` says what to fix. A dry run is in development. With `dryRun: true`, `schedule_post` and `publish_post_now` run every check and book or publish nothing. `blocked` lists the destinations that would fail. An empty list is not a guarantee: a platform can still refuse when the post goes out. `reschedule_posts` runs each entry on its own. A refused entry shows its error in `moves[].error`, and the others still run. Do not resend the whole list. `publish_post_now` is safe to call again. It retries only the destinations that failed and never sends a published one twice. A destination that comes back `pending` with `publishing_in_progress` is still finishing at the platform, as Instagram routinely does. Do not retry it: poll `get_post_status`. To pull one destination back from its booking, call `unschedule_platform` with its `platformPostId`, which `get_post_status` returns. A post with three destinations takes three calls. The destination keeps its date as a draft. > **Read before you retry a write.** A resent call is a new attempt. The gateway does not deduplicate it, and idempotency keys of your own are not available yet. After a timeout or a dropped connection on `create_posts`, `schedule_post` or `publish_post_now`, read the current state first. A lost response does not prove that nothing happened. ## 9. Track what happens MCP tool `get_post_status` (preview) MCP tool `get_profile` (preview) Native does not retry a failed publish by itself, and there are no webhooks yet. Poll `get_post_status`. It reports each destination’s `state`, its live `url` and, on a failure, an `error` with a `code` and `retryable`. | After | How often | Stop when | | --- | --- | --- | | A scheduled time | Once a few minutes after `scheduledAt`, then every 5 minutes for 30 minutes | Every destination is `published` or `failed` | | `publish_post_now` | Every 10 seconds for 2 minutes, then every minute for 10 minutes | Every destination is `published` or `failed` | A destination in `stuck` was accepted, its time passed and no outcome came back. Stop polling and contact Native support: a retry could post it twice. When a destination fails with `channel_needs_reauth` or `channel_not_connected`, send the client a fresh connect link, then retry. For other codes, follow `retryable`. Check channel health with `get_profile` every day and before you schedule. Act on `needs_reauth` and `disconnected`. Act on `expiring_soon` too: the channel still publishes, but only until its token runs out. ## 10. Offboard the client MCP tool `deactivate_profile` (in development) MCP tool `delete_profile` (in development) `deactivate_profile` ends service and deletes nothing. Every scheduled post goes back to unscheduled and keeps its date. The client’s social accounts are disconnected and removed at the provider. Every profile in the workspace becomes inactive, and the workspace loses its plan, so nothing can be generated, scheduled or published for it. There is no tool to reactivate a client. Reconnecting means the client logs in with each platform again. `delete_profile` erases the client permanently: the workspace, every profile in it, its posts, pictures, videos and uploaded files, its brand settings and its onboarding data. There is no undo, and Native keeps no copy. Use it for a GDPR erasure request. Afterwards, the client’s image URLs stop working. delete_profile arguments: ```json { "brandId": "1f461fa4-a41c-4efd-81ba-2946ca61d479", "confirm": true } ``` Both run in the background. The first call schedules the work. Call again with the same arguments about every 30 seconds, until `status` is `deactivated` or `deleted`. That usually takes minutes. Repeating either call is always safe. Both refuse with `stripe_billed_workspace` when the workspace pays for its own Native subscription. That subscription has to be cancelled in Native. ## How long things take | Wait | How long | How to follow it | | --- | --- | --- | | Profile build | Typically 10 to 30 minutes | `get_profile`, about once a minute | | Connect link | Expires about 10 minutes after you mint it. A `connectToken` lasts about as long | Mint a fresh link when it runs out | | Post generation | Raise an alert after about 15 minutes | `get_post_batch_status`, every 5 to 30 seconds | | Image render | A template render (in development) takes a few minutes | `get_generation_job`, which waits up to about 24 seconds per call | | One call | Up to 120 seconds for a write that contacts a platform | A client timeout above that, such as 150 seconds | | Offboarding | Usually minutes | The same call again, every 30 seconds | ## What is not there yet - **Partner access.** Sign-in with client credentials and the pinned `/mcp/v1` endpoint are in development. - **Webhooks.** They are planned for finished generation, approval changes, publish results and disconnected accounts. Until then, poll. - **Idempotency keys of your own.** A resent call is a new attempt. Read the state before you retry a write. - **Usage per client.** A partner usage endpoint is planned. It is not available yet. - **Subtitles.** The digest covers a video’s subtitle files, but the API cannot show them yet. ## Where to go next - [Overview](https://docs.native.no/native-api): What the Native API is for, and what is ready. - [Authentication](https://docs.native.no/authentication): Getting and caching a client-credentials token. - [Errors](https://docs.native.no/native-api/errors): Every error code, and whether a retry can help. - [Limits and versions](https://docs.native.no/native-api/limits): Batch sizes, call times, upload sizes, the Native tokens on your plan, and versions.