Nodaro ドキュメント
ドキュメントノードリファレンスモデルAI エージェント(MCP)開発者向けセルフホスティングリサーチ
埋め込み

ミニアプリの埋め込み

公開された Nodaro のミニアプリを、埋め込みコードで、または REST API 上に作った独自のフォームで、自分のプロダクトの中で実行します。入力の読み取りから、実行、結果、エラーまでを扱います。

ミニアプリを埋め込むとは、すでに公開されている Nodaro のアプリを、自分のプロダクトから実行することです。アプリの埋め込みコードをページに貼り付けることも、REST API の上に自分のフォームを構築することもできます。アプリの入力を読み取り、実行を開始し、ポーリングし、結果を表示します。アプリ自体は Nodaro の中にとどまり、自分のプロダクトは入力を集め、出力を表示します。

埋め込みの 2 つの方法

埋め込みコード独自のインターフェース
手間iframe タグを 1 つ貼り付けるだけです。フォームと、小さなサーバールートを構築します。
見た目Nodaro アプリの画面です。自分のデザインです。
実行の名義閲覧者本人で、その人自身の Nodaro セッションを使います。トークンに紐づくアカウントです。

埋め込みコードを貼り付ける

アプリの埋め込み設定を開く

Nodaro で、ミニアプリを開き、マイミニアプリを選んで、アプリのカードにある埋め込みをクリックします。

自分のドメインを許可する

埋め込みを許可するドメインの欄に、https://example.com のような自分のサイトのオリジンを入力し、追加をクリックします。ドメインを 1 つ以上追加するまで、埋め込みはブロックされます。

コードを自分のページにコピーする

埋め込みコードをコピーをクリックし、タグを自分のページに貼り付けます。

<iframe src="https://app.nodaro.ai/embed/your-app-slug" width="100%" height="600" frameborder="0" allow="clipboard-write"></iframe>

独自のインターフェースを構築する

このページの残りの部分では、REST API を使って、公開されたアプリのための独自の Web インターフェースやモバイルインターフェースを構築します。この内容は単体で完結しているため、AI コードジェネレーターにそのまま渡すこともできます。このページの URL に .md を追加すると、Markdown 版が手に入ります。

最初に知っておきたい 3 つのこと

ベース URLNodaro Cloud では https://app.nodaro.ai、セルフホスティング環境では自分のインストール先のアドレスです。
アプリ識別子slug です。公開されたアプリの URL の最後の部分です。https://app.nodaro.ai/app/my-cool-app の場合、スラッグは my-cool-app です。
実行の形非同期です。POST /v1/app/{slug}/run はすぐに runId を返し、結果は GET /v1/app/{slug}/runs/{runId} をポーリングして受け取ります。実行からのコールバックはありません。

トークンを必要としない 2 つの公開エンドポイントが、フォームに必要なすべての情報を教えてくれます。

  • GET /v1/app/{slug} は、アプリの入力とメタデータを返します。
  • GET /v1/nodes/{type} は、1 つのノードタイプのフィールドを返します。

ステップ 1:アプリの入力を読み取る

curl https://app.nodaro.ai/v1/app/<slug>

自分のインターフェースにとって重要なフィールドは、次のとおりです。

{
  "id": "uuid",
  "name": "Headline Generator",
  "description": "...",
  "iconUrl": "https://...",
  "version": 3,                        // the latest version
  "estimatedCredits": 5,               // the credits one run costs
  "maxRunsPerUserPerDay": null,        // or a number
  "thumbnailNodeId": "node-abc",       // the node whose output is the main result
  "snapshotNodes": [                   // the workflow's nodes
    {
      "id": "node-abc",
      "type": "generate-image",        // use it in step 2
      "data": {
        "prompt": "default prompt",    // the current value of each field
        "aspectRatio": "1:1"
      }
    }
  ],
  "snapshotEdges": [ /* the connections, rarely needed by your interface */ ],
  "snapshotSettings": {
    "presentationSettings": {
      "inputItems": [                  // the form schema
        { "type": "field", "id": "item-1", "nodeId": "node-abc", "field": "prompt", "allowedValues": null },
        { "type": "field", "id": "item-2", "nodeId": "node-abc", "field": "aspectRatio", "allowedValues": ["1:1", "16:9", "9:16"] }
      ]
    }
  },
  "versions": [{ "version": 3, "id": "...", "createdAt": "..." }]
}

inputItems は、公開者が設計したフォームです。これをたどり、すべての group の中に入り、すべての field 項目を集めます。それが、自分のフォームになります。

type描画方法
fieldフォームの入力項目です。nodeId、field、任意の allowedValues を読み取ります。
nodeノード全体のデフォルトブロックです。カスタムフォームでは省略するか、そのノードのすべてのフィールドを描画します。
output結果のライブプレビューです。フォームを構築している間は無視し、実行後に表示します。
richtext公開者が書いた、静的な Markdown です。そのまま描画します。
group独自の items を持つコンテナです。この中に入って処理します。グループはネストしません。

ステップ 2:各フィールドの型を調べる

inputItems は nodeId と field の組を教えてくれますが、テキスト、数値、選択肢といったフィールドの型までは教えてくれません。詳しく知るには、ノードタイプを調べます。

curl https://app.nodaro.ai/v1/nodes/generate-image
{
  "data": {
    "type": "generate-image",
    "label": "Generate Image",
    "category": "ai-image",
    "description": "...",
    "outputType": "image",
    "providers": ["nano-banana-pro", "gpt-image-2", "flux"],   // shortened
    "capabilities": ["supports-reference-image", "supports-negative-prompt"]
  }
}

すべてのノードの記述子が、完全な inputSchema を持っているわけではありません。ない場合は、次のルールを順番に適用して、コントロールを推測します。

  1. 入力項目に allowedValues がある場合:それらの選択肢を持つセレクトです。
  2. snapshotNodes[i].data[field] の現在値が真偽値の場合:トグルです。
  3. 現在値が数値の場合:数値入力です。フィールド名が duration、intensity、strength、scale、temperature のいずれかで終わる場合は、スライダーです。
  4. 現在値が文字列で、フィールド名が prompt、description、text、content、caption、message のいずれかの場合:複数行のテキストエリアです。
  5. 現在値が URL で、ノードタイプが upload- で始まる場合:ファイルの公開 URL を送信する、ファイルアップロードです。
  6. それ以外:1 行のテキスト入力です。

次のフィールド名で、公開されるフィールドの大半をカバーできます。

フィールド名コントロール備考
prompt、negativePrompt、text、descriptionテキストエリア
model、provider、voice、style、toneセレクト選択肢は allowedValues または providers から取得します。
aspectRatioセレクト通常は 1:1、16:9、9:16、4:3、3:4 です。
resolutionセレクト通常は 1K、2K、4K、または 720p、1080p、4k です。
qualityセレクト通常は medium と high です。
duration、nFrames、seed、temperature数値
enableTranslation、addAudio、headless、loopトグル
imageUrl、videoUrl、audioUrl、referenceImageURL先にファイルをアップロードし、その URL を送信します。

Lottie のスロットフィールド

Lottie エンジンを使う モーショングラフィックス(Motion Graphics) ノードは、色、テキスト、数値といった名前付きのスロットを、アプリの入力として公開できます。これらは、slot:primaryColor のように、field が slot: で始まるフィールド項目として現れます。ユーザーは、実行のたびにこれらを 0 クレジットで変更できます。アプリがスロットフィールドを公開している場合、すべての実行が公開済みのアニメーションを再利用し、スロットの値だけを入れ替えるためです。

スロットの値コントロール送信する値
0 から 1 までの数値からなる RGBA 配列としての色カラーピッカー"#00ff00" のような 16 進数の文字列
文字列テキスト入力その文字列
数値スライダーその数値

スロットの値は、単純なノードのフィールドではなく、そのノードの motionPlan.slotValues の中にあります。生の inputOverrides を通じてスロットを設定するには、ノードの motionPlan 全体を、公開済みのプランのコピーで置き換え、その slotValues に変更内容を持たせます。色は RGBA 配列で指定します。slotValues の部分的なパッチでは、プランの残りの部分が失われてしまいます。Nodaro アプリと SDK は、これを自動で組み立てます。

{
  "inputOverrides": {
    "node-mg1": {
      "motionPlan": {
        // ...the published node's motionPlan, unchanged...
        "slotValues": { "primaryColor": [0, 1, 0, 1], "nameText": "Acme Inc." }
      }
    }
  }
}

ステップ 3:フォームを構築する

// Adapt to your framework.
type FormField = {
  nodeId: string
  field: string
  label: string                     // "aspectRatio" becomes "Aspect ratio"
  control: "text" | "textarea" | "number" | "toggle" | "select" | "upload"
  options?: Array<string | number | boolean>
  defaultValue: unknown
}

async function buildForm(slug: string, baseUrl: string): Promise<FormField[]> {
  const app = await fetch(`${baseUrl}/v1/app/${slug}`).then((r) => r.json())
  const nodesById = new Map(app.snapshotNodes.map((n) => [n.id, n]))

  const fields: FormField[] = []
  const walk = (items) => {
    for (const it of items ?? []) {
      if (it.type === "group") walk(it.items)
      if (it.type !== "field") continue
      const node = nodesById.get(it.nodeId)
      const current = node?.data?.[it.field]
      fields.push(toFormField(it, node, current))
    }
  }
  walk(app.snapshotSettings?.presentationSettings?.inputItems ?? [])
  return fields
}
  • 値は、ラベルではなくノード ID をキーにします。実行のボディは { "inputOverrides": { "<nodeId>": { "<field>": value } } } です。
  • Nodaro は allowedValues を強制します。リストにない値は 400 validation_error で拒否されます。
  • デフォルト値は、snapshotNodes[i].data からあらかじめ入力しておきます。何も変更しないユーザーでも、意味のある実行になるようにするためです。
  • estimatedCredits を表示し、実行前にユーザーが料金を把握できるようにします。
  • maxRunsPerUserPerDay が設定されている場合は、「本日は 5 回中 2 回使用済み」のように、1 日の上限を表示し、予期しない 429 を避けます。

ステップ 4:実行を開始してポーリングする

トークンを使って実行を開始します。

curl -X POST https://app.nodaro.ai/v1/app/<slug>/run \
  -H "Authorization: Bearer $NODARO_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "inputOverrides": {
      "node-abc": { "prompt": "a cat astronaut", "aspectRatio": "16:9" }
    }
  }'

応答は 202 Accepted です。

{ "executionId": "exec-uuid", "runId": "run-uuid", "status": "pending" }
任意のボディフィールド内容
versionアプリの指定したバージョンを実行します。デフォルトは最新版です。
inputsアプリの入力を、入力名から値へのフラットなマップとして渡します。SDK と CLI が送る形式です。inputOverrides はフィールドごとに inputs の上に適用され、両方が同じフィールドを設定している場合は inputOverrides が優先されます。
runId実行を、以前に作成した下書きの実行に紐付けます。最初の構築では無視してかまいません。

inputOverrides は、promptPrefix のような、アプリが公開していないノードのフィールドも設定できます。プロンプトの前後のテキストを参照してください。ただし、送信系のノードがデータを送信または取得する宛先(Webhook 出力(Webhook Output)のアドレスなど)は、決して変更できません。そのような実行は 400 locked_field で拒否されます。

続けて、実行をポーリングします。

curl https://app.nodaro.ai/v1/app/<slug>/runs/<runId> \
  -H "Authorization: Bearer $NODARO_API_TOKEN"
{
  "id": "run-uuid",
  "executionId": "exec-uuid",
  "status": "pending|running|completed|failed",
  "creditsUsed": 0,
  "thumbnailUrl": null,                       // set when the run completes
  "execution": {
    "status": "pending|running|completed|failed",
    "nodeStates": {
      "node-abc": {
        "status": "completed",
        "output": {                           // the shape depends on the node's outputType
          "url": "https://.../result.png",
          "imageUrl": "...",
          "videoUrl": "...",
          "audioUrl": "...",
          "resultUrl": "...",
          "text": "..."
        }
      }
    },
    "totalNodes": 5,
    "completedNodes": 5,
    "failedNodes": 0,
    "totalCreditsUsed": 5,
    "errorMessage": null,
    "completedAt": "2026-05-07T..."
  }
}
  • 2 秒ごとを目安にポーリングします。execution.status が completed または failed になったら止めます。
  • 実行中は、totalNodes に対する completedNodes として、進行状況を表示します。
  • 失敗した場合は、execution.errorMessage を表示します。

ステップ 5:結果を表示する

メインの結果は、thumbnailNodeId が指すノードの出力です。その URL は execution.nodeStates[thumbnailNodeId].output から読み取り、url、imageUrl、videoUrl、audioUrl、resultUrl の順に試し、最後に、テキストの結果については text を試します。text はリンクではなく、テキストとして表示します。

GET /v1/nodes/{type} から得られる、ノードの outputType に応じて描画します。

outputType描画方法
image画像
videoコントロール付きの動画プレーヤー
audioコントロール付きの音声プレーヤー
text整形済みテキストまたは Markdown
dataJSON ビューアー、または中身を解釈しないデータ

thumbnailNodeId が null の場合は、ワークフローの順序で最後のノードを使うか、空でない出力をすべて表示します。

実行を削除する

curl -X DELETE https://app.nodaro.ai/v1/app/<slug>/runs/<runId> \
  -H "Authorization: Bearer $NODARO_API_TOKEN"

削除は、実行をユーザーのアーカイブに移し、{ "success": true, "archived": true } で応答します。何かが破棄されることはありません。誤った実行を削除する自動化があっても、ユーザーのデータが失われることはありません。実行の復元と完全な削除は、Nodaro アプリでしかできないため、自分の連携では、どちらも試みるべきではありません。

認証する

実行には、必ずベアラートークンが必要です。実行の所有者と支払い者が誰であるべきかによって、種類を選びます。

構築しているもの使うもの
個人用ツール、代理店向けダッシュボード、社内の自動化個人用 API トークン。
顧客がそれぞれ自分の Nodaro アカウントを接続する SaaSOAuth。
匿名の訪問者のためにアプリを実行する、公開ツール個人用 API トークン。支払うのは自分なので、訪問者ごとの実行回数は自分の側で制限します。

個人用 API トークン。自分のアカウントが、すべての実行を所有し、料金を支払います。設定 › APIトークンを開き、トークンを作成でトークンを作成します。表示されたトークンをその場でコピーし、サーバーのシークレットとして保存します。トークンは、取り消すまで有効です。認証を参照してください。

OAuth。各ユーザー自身のアカウントが、それぞれの実行を所有し、料金を支払います。開発者アプリを登録し、実行を開始するための workflows:execute と、それをポーリングするための jobs:read のスコープを添えて、ユーザーを同意画面に送ります。自分のサーバーで、認可コードを、90 日間有効なアクセストークンと交換します。呼び出しが 403 insufficient_scope を返す場合は、より広いスコープで、ユーザーにもう一度同意画面を通ってもらいます。

トークンはサーバーに置く

トークンは、決してブラウザーに渡してはいけません。バンドラーは、VITE_ や NEXT_PUBLIC_ のプレフィックスが付いた環境変数を、配布する JavaScript にそのまま埋め込みます。そうなると、開発者ツールを使える人なら誰でもトークンを読み取り、あなたのクレジットを使えてしまいます。

[Browser]  --HTTPS-->  [Your server or edge function]  --HTTPS-->  Nodaro API
                         (holds NODARO_API_TOKEN)
スタックトークンの置き場所
Supabase を使う LovableSupabase Edge Function のシークレットです。supabase secrets set NODARO_API_TOKEN=...
Next.jsprocess.env.NODARO_API_TOKEN を使う、サーバールートハンドラーです。NEXT_PUBLIC_ プレフィックスは付けません。
SvelteKit、Remix、Nuxtサーバー専用の環境変数です。SvelteKit の $env/static/private などです。
Vercel または Netlify のエッジ関数クライアントに公開されない、プロジェクトの環境変数です。
Cloudflare WorkerWorker のシークレットです。wrangler secret put NODARO_API_TOKEN

ブラウザーのコードは自分のサーバーとしか通信しないため、自分のドメインを Nodaro の許可するオリジンに追加する必要はありません。OAuth を使ってブラウザーから Nodaro を呼び出す場合は、開発者アプリの許可するオリジンに、自分のオリジンを登録してください。

エラー

エラーは { "error": { "code": "...", "message": "..." } } の形をしています。

ステータスコード原因対処
400validation_error不正な形式の inputOverrides、allowedValues にない値、または不正な形式のスラッグです。リクエストボディを修正します。
400locked_fieldオーバーライドが、送信系のノードの宛先(Webhook 出力のアドレスなど)を指定しています。それを取り除きます。送信先と取得先は、アプリが決めます。
401unauthorizedトークンがないか、期限切れか、取り消されています。新しいトークンを作成するか、もう一度認可します。
402insufficient_app_creditsトークンに紐づくアカウントのクレジットが不足しています。クレジットを追加するか、プランを変更します。
403insufficient_scopeOAuth トークンに、missingScope に示されたスコープがありません。より広いスコープで、もう一度認可します。
404not_foundスラッグまたは実行が存在しないか、アプリが無効化されています。スラッグを確認し、「アプリを利用できません」と表示します。
429rate_limit_exceededアプリの、ユーザーあたりの 1 日の実行上限に達しています。「本日の上限に達しました」と表示します。
500internal_errorサーバー側の不具合です。少し待ってから 1 回再試行し、それでも解決しない場合は報告します。

参考テンプレート

アプリをプロキシする Supabase Edge Function

// supabase/functions/nodaro-run/index.ts
import { serve } from "https://deno.land/std@0.224.0/http/server.ts"

const BASE = Deno.env.get("NODARO_BASE_URL")!       // for example https://app.nodaro.ai
const TOKEN = Deno.env.get("NODARO_API_TOKEN")!     // ndr_...
const SLUG = Deno.env.get("NODARO_APP_SLUG")!       // my-cool-app

serve(async (req) => {
  const url = new URL(req.url)

  // Public probe: no token is sent, because the endpoint is public.
  if (req.method === "GET" && url.pathname.endsWith("/schema")) {
    const r = await fetch(`${BASE}/v1/app/${SLUG}`)
    return new Response(await r.text(), { status: r.status, headers: { "content-type": "application/json" } })
  }

  // Start a run.
  if (req.method === "POST" && url.pathname.endsWith("/run")) {
    const { inputs } = await req.json()
    const r = await fetch(`${BASE}/v1/app/${SLUG}/run`, {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${TOKEN}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ inputOverrides: inputs }),
    })
    return new Response(await r.text(), { status: r.status, headers: { "content-type": "application/json" } })
  }

  // Poll a run.
  const m = url.pathname.match(/\/runs\/([0-9a-f-]{36})$/)
  if (req.method === "GET" && m) {
    const r = await fetch(`${BASE}/v1/app/${SLUG}/runs/${m[1]}`, {
      headers: { "Authorization": `Bearer ${TOKEN}` },
    })
    return new Response(await r.text(), { status: r.status, headers: { "content-type": "application/json" } })
  }

  return new Response("Not found", { status: 404 })
})

ブラウザーのコード:読み取り、実行、ポーリング

// 1. On mount: read the schema and build the form (steps 1 to 3).
const schema = await fetch("/functions/v1/nodaro-run/schema").then((r) => r.json())
const inputItems = schema.snapshotSettings?.presentationSettings?.inputItems ?? []

// 2. On submit: send the values keyed by node id.
const start = await fetch("/functions/v1/nodaro-run/run", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    inputs: { "node-abc": { prompt: form.prompt, aspectRatio: form.aspectRatio } },
  }),
}).then((r) => r.json())

// 3. Poll every 2 seconds until the run ends.
let last = start
while (last.status !== "completed" && last.execution?.status !== "completed"
       && last.status !== "failed" && last.execution?.status !== "failed") {
  await new Promise((res) => setTimeout(res, 2000))
  last = await fetch(`/functions/v1/nodaro-run/runs/${start.runId}`).then((r) => r.json())
}

// 4. Read the result.
const out = last.execution?.nodeStates?.[schema.thumbnailNodeId]?.output ?? {}
const heroUrl = out.url ?? out.imageUrl ?? out.videoUrl ?? out.audioUrl ?? out.resultUrl
const heroText = out.text

このテンプレートでは、ブラウザーがノード ID をキーにした値を送信し、エッジ関数がそれを inputOverrides として渡します。

インターフェースのコードを生成する前のチェックリスト

  • GET {BASE}/v1/app/{slug} が、入力スキーマとデフォルト値を返した。
  • inputItems 内の各 nodeId について、snapshotNodes からその type がわかっている。
  • 各ノードタイプについて、GET {BASE}/v1/nodes/{type} から、カテゴリー、outputType、モデルがわかった。
  • ステップ 2 のルールで、フォームフィールドの一覧を構築した。
  • 結果のノード(thumbnailNodeId または最後のノード)と、その outputType がわかっている。
  • 個人用 API トークンか、開発者アプリの認証情報を持っている。
  • シークレットは自分のサーバーに置かれている。

よくある質問

最終更新

目次