# Incorporar um miniapp

> Execute um miniapp do Nodaro no seu produto com o código de incorporação ou um formulário próprio na API REST: entradas, execuções, resultados e erros.

Source: https://nodaro.ai/pt-BR/docs/developers/embed/miniapps

**Incorporar um miniapp** é executar um app do Nodaro já publicado a partir do seu próprio produto. Você pode colar o código de incorporação do app em uma página, ou criar seu próprio formulário sobre a API REST: ler as entradas do app, iniciar uma execução, consultá-la periodicamente e mostrar o resultado. O app em si fica no Nodaro; o seu produto coleta as entradas e exibe as saídas.

## Duas formas de incorporar
| | Código de incorporação | Sua própria interface |
| --- | --- | --- |
| **Esforço** | Colar uma tag `iframe`. | Criar um formulário e uma pequena rota de servidor. |
| **Visual** | A visualização de app do Nodaro. | O seu próprio design. |
| **Em nome de quem a execução é feita** | De quem está vendo a página, com a própria sessão do Nodaro. | Da conta por trás do seu token. |

## Colar o código de incorporação
### Abrir as configurações de incorporação do app
No Nodaro, abra **Miniapps**, escolha **Meus miniapps** e clique em **Incorporar** no card do app.

### Permitir o seu domínio
Em **Domínios permitidos para incorporação**, digite a origem do seu site, como `https://example.com`, e clique em **Adicionar**. A incorporação fica bloqueada até que pelo menos um domínio seja adicionado.

### Copiar o código para a sua página
Clique em **Copiar código de incorporação** e cole a tag na sua página:

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

## Criar sua própria interface
O restante desta página cria uma interface web ou mobile personalizada para um app publicado, sobre a API REST. O conteúdo é autossuficiente, então você também pode entregá-lo a um gerador de código com IA: adicione `.md` à URL desta página para obter uma cópia em Markdown.

### Três coisas para saber antes
| | |
| --- | --- |
| **URL base** | `https://app.nodaro.ai` no Nodaro Cloud, ou o endereço da sua própria instalação. |
| **Identificador do app** | O `slug`: a última parte da URL do app publicado. Para `https://app.nodaro.ai/app/my-cool-app`, o slug é `my-cool-app`. |
| **Formato da execução** | Assíncrono. `POST /v1/app/{slug}/run` responde na hora com um `runId`, e você consulta `GET /v1/app/{slug}/runs/{runId}` periodicamente para obter o resultado. A execução não faz callback. |

Dois endpoints públicos, que não precisam de token, informam tudo de que você precisa para o formulário:

- `GET /v1/app/{slug}` retorna as entradas e os metadados do app.
- `GET /v1/nodes/{type}` retorna os campos de um tipo de nó.

### Etapa 1: ler as entradas do app
```bash
curl https://app.nodaro.ai/v1/app/<slug>
```

Os campos que importam para a sua interface:

```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` é o formulário que o publicador montou. Percorra a lista, entre em cada `group` e colete todos os itens `field`: esse é o seu formulário.

| `type` | Renderize como |
| --- | --- |
| `field` | Um campo de formulário. Leia `nodeId`, `field` e o `allowedValues` opcional. |
| `node` | O bloco padrão do nó inteiro. Ignore-o em um formulário personalizado, ou renderize todos os campos do nó. |
| `output` | Uma prévia ao vivo de um resultado. Ignore-a enquanto monta o formulário e mostre-a depois da execução. |
| `richtext` | Markdown estático que o publicador escreveu. Renderize-o como está. |
| `group` | Um contêiner com os próprios `items`. Entre nele; os grupos não se aninham. |

### Etapa 2: descobrir o tipo de cada campo
`inputItems` fornece pares de `nodeId` e `field`, mas não o tipo do campo: texto, número, escolha. Busque o tipo de nó para saber mais:

```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"]
}
}
```

Nem todo descritor de nó tem um `inputSchema` completo. Quando ele estiver ausente, deduza o controle com estas regras, nesta ordem:

1. O item de entrada tem `allowedValues`: um select com essas opções.
2. O valor atual em `snapshotNodes[i].data[field]` é um booleano: um toggle.
3. O valor atual é um número: um campo numérico, ou um slider quando o nome do campo termina em `duration`, `intensity`, `strength`, `scale` ou `temperature`.
4. O valor atual é uma string, e o nome do campo é `prompt`, `description`, `text`, `content`, `caption` ou `message`: uma área de texto com várias linhas.
5. O valor atual é uma URL, e o tipo de nó começa com `upload-`: um upload de arquivo que envia a URL pública do arquivo.
6. Qualquer outra coisa: um campo de texto de uma linha.

Estes nomes de campo cobrem a maioria dos campos expostos:

| Nome do campo | Controle | Observações |
| --- | --- | --- |
| `prompt`, `negativePrompt`, `text`, `description` | Área de texto | |
| `model`, `provider`, `voice`, `style`, `tone` | Select | As opções vêm de `allowedValues` ou de `providers`. |
| `aspectRatio` | Select | Normalmente `1:1`, `16:9`, `9:16`, `4:3` e `3:4`. |
| `resolution` | Select | Normalmente `1K`, `2K` e `4K`, ou `720p`, `1080p` e `4k`. |
| `quality` | Select | Normalmente `medium` e `high`. |
| `duration`, `nFrames`, `seed`, `temperature` | Número | |
| `enableTranslation`, `addAudio`, `headless`, `loop` | Toggle | |
| `imageUrl`, `videoUrl`, `audioUrl`, `referenceImage` | URL | Faça o upload do arquivo primeiro e depois envie a URL dele. |

#### Campos de slot do Lottie
Um nó [**Gráficos animados** (Motion Graphics)](https://nodaro.ai/docs/nodes/video/motion-graphics) com o mecanismo Lottie pode expor os slots nomeados dele, como cores, textos e números, como entradas do app. Eles aparecem como itens de campo cujo `field` começa com `slot:`, como `slot:primaryColor`. Os usuários os alteram a cada execução por **0 créditos**: quando um app expõe campos de slot, cada execução reaproveita a animação publicada e só troca os valores dos slots.

| Valor do slot | Controle | Valor a enviar |
| --- | --- | --- |
| Uma cor, como um array RGBA de números de 0 a 1 | Seletor de cor | Uma string hexadecimal, como `"#00ff00"` |
| Uma string | Campo de texto | A string |
| Um número | Slider | O número |

Um valor de slot não é um campo comum do nó: ele fica em `motionPlan.slotValues` do nó. Para definir slots por `inputOverrides` diretos, substitua o `motionPlan` inteiro do nó por uma cópia do plano publicado cujo `slotValues` traga as suas alterações, com as cores como arrays RGBA. Um patch parcial de `slotValues` descartaria o restante do plano. O app do Nodaro e o SDK montam isso para você.

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

### Etapa 3: montar o formulário
```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
}
```

- **Use o id do nó como chave dos valores, não o rótulo.** O corpo da execução é `{ "inputOverrides": { "<nodeId>": { "<field>": value } } }`.
- **O Nodaro aplica o `allowedValues`.** Um valor fora da lista é recusado com `400 validation_error`.
- **Preencha os valores padrão** a partir de `snapshotNodes[i].data`, para que um usuário que não altere nada ainda tenha uma execução útil.
- **Mostre `estimatedCredits`,** para que os usuários saibam o custo antes de executar.
- **Mostre o limite diário** quando `maxRunsPerUserPerDay` estiver definido, como “2 de 5 execuções usadas hoje”, para evitar um `429` inesperado.

### Etapa 4: iniciar uma execução e consultá-la periodicamente
Inicie a execução com um token:

```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" }
}
}'
```

A resposta é `202 Accepted`:

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

| Campo opcional do corpo | O que faz |
| --- | --- |
| `version` | Executa uma versão específica do app. O padrão é a mais recente. |
| `inputs` | As entradas do app como um mapa plano de nomes de entrada para valores, do jeito que o SDK e a CLI as enviam. `inputOverrides` é aplicado sobre `inputs`, campo por campo, e prevalece onde os dois definem o mesmo campo. |
| `runId` | Vincula a execução a um rascunho de execução que você criou antes. Você pode ignorar este campo em uma primeira versão. |

`inputOverrides` também pode definir campos de nós que o app não expõe, como `promptPrefix`; veja [Texto antes e depois do prompt](https://nodaro.ai/docs/concepts/prompt-pre-post-text). Ele nunca pode mudar para onde um nó externo envia dados ou de onde ele os busca, como o endereço de um nó **Saída de webhook** (Webhook Output): essa execução é recusada com `400 locked_field`.

Depois, consulte a execução periodicamente:

```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..."
}
}
```

- **Consulte periodicamente, mais ou menos a cada 2 segundos.** Pare quando `execution.status` for `completed` ou `failed`.
- **Mostre o progresso** como `completedNodes` de `totalNodes` enquanto a execução estiver em andamento.
- **Em caso de falha,** mostre `execution.errorMessage`.

### Etapa 5: mostrar o resultado
O resultado principal é a saída do nó indicado por `thumbnailNodeId`. Leia a URL dele em `execution.nodeStates[thumbnailNodeId].output`, tentando estas chaves nesta ordem: `url`, `imageUrl`, `videoUrl`, `audioUrl`, `resultUrl` e, por fim, `text` para um resultado de texto, que você mostra como texto e não como link.

Renderize o resultado de acordo com o `outputType` do nó, vindo de `GET /v1/nodes/{type}`:

| `outputType` | Renderize como |
| --- | --- |
| `image` | Uma imagem |
| `video` | Um player de vídeo com controles |
| `audio` | Um player de áudio com controles |
| `text` | Texto pré-formatado ou Markdown |
| `data` | Um visualizador de JSON, ou dados opacos |

Quando `thumbnailNodeId` for `null`, use o último nó na ordem do workflow, ou mostre todas as saídas que não estejam vazias.

### Excluir uma execução
```bash
curl -X DELETE https://app.nodaro.ai/v1/app/<slug>/runs/<runId> \
  -H "Authorization: Bearer $NODARO_API_TOKEN"
```

Uma exclusão move a execução para os itens arquivados do usuário e responde `{ "success": true, "archived": true }`. Nada é destruído: uma automação que exclua a execução errada não consegue perder os dados do usuário. Restaurar uma execução e excluí-la de vez só são possíveis no app do Nodaro, então a sua integração não deve tentar nenhuma das duas coisas.

## Autenticação
Toda execução precisa de um token bearer. Escolha o tipo de acordo com quem deve ser dono das execuções e pagar por elas:

| Você está criando | Use |
| --- | --- |
| Uma ferramenta pessoal, um painel de agência ou uma automação interna | Um token de API pessoal. |
| Um SaaS em que os clientes conectam as próprias contas do Nodaro | [OAuth](https://nodaro.ai/docs/developers/oauth). |
| Uma ferramenta pública que executa um app para visitantes anônimos | Um token de API pessoal. Quem paga é você, então limite as execuções por visitante do seu lado. |

**Token de API pessoal.** A sua conta é dona de todas as execuções e paga por elas. Crie o token em **Configurações › Tokens de API** com **Criar token**, copie-o uma vez e guarde-o como um segredo do servidor. Ele vale até você revogá-lo. Veja [Autenticação](https://nodaro.ai/docs/developers/api/authentication).

**OAuth.** A conta de cada usuário é dona das execuções dele e paga por elas. Registre um app de desenvolvedor e envie o usuário à tela de consentimento com os escopos `workflows:execute`, para iniciar execuções, e `jobs:read`, para consultá-las periodicamente. No seu servidor, troque o código por um token de acesso que vale 90 dias. Se uma chamada responder `403 insufficient_scope`, envie o usuário de novo pela tela de consentimento com os escopos mais amplos.

## Manter o token no seu servidor
**O token nunca deve chegar ao navegador.** Os bundlers embutem toda variável de ambiente com prefixo `VITE_` ou `NEXT_PUBLIC_` no JavaScript que entregam, onde qualquer pessoa com as ferramentas de desenvolvedor pode lê-la e gastar os seus créditos.

```text
[Browser]  --HTTPS-->  [Your server or edge function]  --HTTPS-->  Nodaro API
(holds NODARO_API_TOKEN)
```

| Stack | Onde o token fica |
| --- | --- |
| Lovable com Supabase | Um segredo de Edge Function do Supabase: `supabase secrets set NODARO_API_TOKEN=...` |
| Next.js | Um route handler de servidor, com `process.env.NODARO_API_TOKEN` e sem o prefixo `NEXT_PUBLIC_` |
| SvelteKit, Remix, Nuxt | Uma variável de ambiente só do servidor, como `$env/static/private` no SvelteKit |
| Edge function da Vercel ou da Netlify | Uma variável de ambiente do projeto que não é exposta ao cliente |
| Cloudflare Worker | Um segredo do Worker: `wrangler secret put NODARO_API_TOKEN` |

Como o código no navegador só se comunica com o seu próprio servidor, você não precisa adicionar o seu domínio às origens permitidas do Nodaro. Se você chamar o Nodaro direto do navegador com OAuth, registre a sua origem em **Origens permitidas**, no app de desenvolvedor.

## Erros
Os erros têm o formato `{ "error": { "code": "...", "message": "..." } }`.

| Status | Código | Causa | O que fazer |
| --- | --- | --- | --- |
| `400` | `validation_error` | Um `inputOverrides` malformado, um valor fora de `allowedValues` ou um slug malformado. | Corrija o corpo da requisição. |
| `400` | `locked_field` | Uma substituição indica o destino de um nó externo, como o endereço de uma Saída de webhook. | Remova a substituição. O app decide para onde envia e de onde busca. |
| `401` | `unauthorized` | O token está ausente, expirado ou revogado. | Crie um novo token, ou autorize de novo. |
| `402` | `insufficient_app_credits` | A conta por trás do token tem créditos insuficientes. | Adicione créditos ou mude de plano. |
| `403` | `insufficient_scope` | Falta ao token OAuth um escopo, indicado em `missingScope`. | Autorize de novo com os escopos mais amplos. |
| `404` | `not_found` | O slug ou a execução não existe, ou o app está desativado. | Confira o slug e mostre “App indisponível”. |
| `429` | `rate_limit_exceeded` | O limite diário de execuções do app por usuário foi atingido. | Mostre “Limite diário atingido”. |
| `500` | `internal_error` | Uma falha no servidor. | Tente de novo uma vez depois de uma pausa, e relate o problema se ele persistir. |

## Templates de referência
### Uma Edge Function do Supabase que faz proxy do app
```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 })
})
```

### Código do navegador: ler, executar, consultar periodicamente
```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
```

Neste template, o navegador envia valores com o id do nó como chave, e a edge function os repassa como `inputOverrides`.

### Checklist antes de gerar o código da interface
- `GET {BASE}/v1/app/{slug}` retornou o schema de entrada e os valores padrão.
- Para cada `nodeId` em `inputItems`, você sabe o `type` dele a partir de `snapshotNodes`.
- Para cada tipo de nó, `GET {BASE}/v1/nodes/{type}` informou a categoria, o `outputType` e os modelos dele.
- Você montou a lista de campos do formulário com as regras da etapa 2.
- Você sabe qual é o nó do resultado, `thumbnailNodeId` ou o último nó, e o `outputType` dele.
- Você tem um token de API pessoal ou as credenciais de um app de desenvolvedor.
- O segredo fica no seu servidor.

## Frequently asked questions

### Como incorporo um miniapp do Nodaro no meu site?

Abra Miniapps, escolha Meus miniapps e clique em Incorporar no card do app. Adicione o domínio do seu site em Domínios permitidos para incorporação, clique em Copiar código de incorporação e cole o iframe na sua página. A incorporação fica bloqueada até que pelo menos um domínio seja adicionado.

### De quais endpoints preciso para criar minha própria interface de miniapp?

GET /v1/app/{slug} e GET /v1/nodes/{type} para conhecer as entradas, os dois públicos. Depois, POST /v1/app/{slug}/run para iniciar uma execução e GET /v1/app/{slug}/runs/{runId} para consultá-la periodicamente, os dois com um token.

### O Nodaro chama o meu servidor quando a execução de um miniapp termina?

Não. Uma execução não faz callback. Consulte a execução periodicamente, mais ou menos a cada 2 segundos, até o status dela ser completed ou failed.

### Quem paga pelas execuções de um miniapp incorporado?

A conta em nome da qual a execução é feita. Com um token de API pessoal, a sua conta paga por todas as execuções. Com OAuth, a conta de cada usuário paga pelas execuções dele.

### Posso excluir a execução de um miniapp pela API?

Sim. Um DELETE na execução a arquiva, e o usuário pode recuperá-la no app do Nodaro. Restaurar e excluir de vez só são possíveis no app do Nodaro.
