# ミニアプリの埋め込み

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

Source: https://nodaro.ai/ja/docs/developers/embed/miniapps

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

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

## 埋め込みコードを貼り付ける
### アプリの埋め込み設定を開く
Nodaro で、**ミニアプリ**を開き、**マイミニアプリ**を選んで、アプリのカードにある**埋め込み**をクリックします。

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

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

```html
<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：アプリの入力を読み取る
```bash
curl https://app.nodaro.ai/v1/app/<slug>
```

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

```jsonc
{
"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` の組を教えてくれますが、テキスト、数値、選択肢といったフィールドの型までは教えてくれません。詳しく知るには、ノードタイプを調べます。

```bash
curl https://app.nodaro.ai/v1/nodes/generate-image
```

```jsonc
{
"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`、`referenceImage` | URL | 先にファイルをアップロードし、その URL を送信します。 |

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

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

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

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

### ステップ 3：フォームを構築する
```ts
// 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：実行を開始してポーリングする
トークンを使って実行を開始します。

```bash
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` です。

```json
{ "executionId": "exec-uuid", "runId": "run-uuid", "status": "pending" }
```

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

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

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

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

```jsonc
{
"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` の場合は、ワークフローの順序で最後のノードを使うか、空でない出力をすべて表示します。

### 実行を削除する
```bash
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](https://nodaro.ai/docs/developers/oauth)。 |
| 匿名の訪問者のためにアプリを実行する、公開ツール | 個人用 API トークン。支払うのは自分なので、訪問者ごとの実行回数は自分の側で制限します。 |

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

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

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

```text
[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
```ts
// supabase/functions/nodaro-run/index.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 })
})
```

### ブラウザーのコード：読み取り、実行、ポーリング
```ts
// 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 トークンか、開発者アプリの認証情報を持っている。
- シークレットは自分のサーバーに置かれている。

## Frequently asked questions

### Nodaro のミニアプリを、自分のサイトに埋め込むには、どうすればよいですか？

「ミニアプリ」を開き、「マイミニアプリ」を選んで、アプリのカードにある「埋め込み」をクリックします。「埋め込みを許可するドメイン」に自分のサイトのドメインを追加し、「埋め込みコードをコピー」をクリックして、iframe を自分のページに貼り付けます。ドメインを 1 つ以上追加するまで、埋め込みはブロックされます。

### ミニアプリ用の独自のインターフェースを構築するには、どのエンドポイントが必要ですか？

入力を調べるための GET /v1/app/{slug} と GET /v1/nodes/{type} で、どちらも公開エンドポイントです。続いて、実行を開始するための POST /v1/app/{slug}/run と、それをポーリングするための GET /v1/app/{slug}/runs/{runId} を使います。この 2 つにはトークンが必要です。

### ミニアプリの実行が終わると、Nodaro は自分のサーバーを呼び出しますか？

いいえ。実行からのコールバックはありません。ステータスが completed か failed になるまで、2 秒ごとを目安に実行をポーリングしてください。

### 埋め込まれたミニアプリの実行の料金は、誰が支払いますか？

実行の名義となるアカウントです。個人用 API トークンを使う場合は、自分のアカウントがすべての実行の料金を支払います。OAuth を使う場合は、各ユーザー自身のアカウントが、それぞれの実行の料金を支払います。

### API 経由で、ミニアプリの実行を削除できますか？

はい。実行に対する DELETE は、それをアーカイブし、ユーザーは Nodaro アプリでそれを復元できます。復元と完全な削除は、Nodaro アプリでしかできません。
