ミニアプリの埋め込み
公開された 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 つのこと
| ベース URL | Nodaro 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 を持っているわけではありません。ない場合は、次のルールを順番に適用して、コントロールを推測します。
- 入力項目に
allowedValuesがある場合:それらの選択肢を持つセレクトです。 snapshotNodes[i].data[field]の現在値が真偽値の場合:トグルです。- 現在値が数値の場合:数値入力です。フィールド名が
duration、intensity、strength、scale、temperatureのいずれかで終わる場合は、スライダーです。 - 現在値が文字列で、フィールド名が
prompt、description、text、content、caption、messageのいずれかの場合:複数行のテキストエリアです。 - 現在値が URL で、ノードタイプが
upload-で始まる場合:ファイルの公開 URL を送信する、ファイルアップロードです。 - それ以外: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、referenceImage | URL | 先にファイルをアップロードし、その 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 |
data | JSON ビューアー、または中身を解釈しないデータ |
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 アカウントを接続する SaaS | OAuth。 |
| 匿名の訪問者のためにアプリを実行する、公開ツール | 個人用 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 を使う Lovable | Supabase Edge Function のシークレットです。supabase secrets set NODARO_API_TOKEN=... |
| Next.js | process.env.NODARO_API_TOKEN を使う、サーバールートハンドラーです。NEXT_PUBLIC_ プレフィックスは付けません。 |
| SvelteKit、Remix、Nuxt | サーバー専用の環境変数です。SvelteKit の $env/static/private などです。 |
| Vercel または Netlify のエッジ関数 | クライアントに公開されない、プロジェクトの環境変数です。 |
| Cloudflare Worker | Worker のシークレットです。wrangler secret put NODARO_API_TOKEN |
ブラウザーのコードは自分のサーバーとしか通信しないため、自分のドメインを Nodaro の許可するオリジンに追加する必要はありません。OAuth を使ってブラウザーから Nodaro を呼び出す場合は、開発者アプリの許可するオリジンに、自分のオリジンを登録してください。
エラー
エラーは { "error": { "code": "...", "message": "..." } } の形をしています。
| ステータス | コード | 原因 | 対処 |
|---|---|---|---|
400 | validation_error | 不正な形式の inputOverrides、allowedValues にない値、または不正な形式のスラッグです。 | リクエストボディを修正します。 |
400 | locked_field | オーバーライドが、送信系のノードの宛先(Webhook 出力のアドレスなど)を指定しています。 | それを取り除きます。送信先と取得先は、アプリが決めます。 |
401 | unauthorized | トークンがないか、期限切れか、取り消されています。 | 新しいトークンを作成するか、もう一度認可します。 |
402 | insufficient_app_credits | トークンに紐づくアカウントのクレジットが不足しています。 | クレジットを追加するか、プランを変更します。 |
403 | insufficient_scope | OAuth トークンに、missingScope に示されたスコープがありません。 | より広いスコープで、もう一度認可します。 |
404 | not_found | スラッグまたは実行が存在しないか、アプリが無効化されています。 | スラッグを確認し、「アプリを利用できません」と表示します。 |
429 | rate_limit_exceeded | アプリの、ユーザーあたりの 1 日の実行上限に達しています。 | 「本日の上限に達しました」と表示します。 |
500 | internal_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 トークンか、開発者アプリの認証情報を持っている。
- シークレットは自分のサーバーに置かれている。
よくある質問
関連ページ
埋め込み
アプリ(ミニアプリ)
OAuth アプリ
認証
TypeScript SDK
最終更新