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.
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:
<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
curl https://app.nodaro.ai/v1/app/<slug>Os campos que importam para a sua interface:
{
"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:
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"]
}
}Nem todo descritor de nó tem um inputSchema completo. Quando ele estiver ausente, deduza o controle com estas regras, nesta ordem:
- O item de entrada tem
allowedValues: um select com essas opções. - O valor atual em
snapshotNodes[i].data[field]é um booleano: um toggle. - O valor atual é um número: um campo numérico, ou um slider quando o nome do campo termina em
duration,intensity,strength,scaleoutemperature. - O valor atual é uma string, e o nome do campo é
prompt,description,text,content,captionoumessage: uma área de texto com várias linhas. - 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. - 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) 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ê.
{
"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
// 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 com400 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
maxRunsPerUserPerDayestiver definido, como “2 de 5 execuções usadas hoje”, para evitar um429inesperado.
Etapa 4: iniciar uma execução e consultá-la periodicamente
Inicie a execução com um token:
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:
{ "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. 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:
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..."
}
}- Consulte periodicamente, mais ou menos a cada 2 segundos. Pare quando
execution.statusforcompletedoufailed. - Mostre o progresso como
completedNodesdetotalNodesenquanto 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
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. |
| 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.
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.
[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
// 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 })
})Código do navegador: ler, executar, consultar periodicamente
// 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.textNeste 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
nodeIdeminputItems, você sabe otypedele a partir desnapshotNodes. - Para cada tipo de nó,
GET {BASE}/v1/nodes/{type}informou a categoria, ooutputTypee 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,
thumbnailNodeIdou o último nó, e ooutputTypedele. - Você tem um token de API pessoal ou as credenciais de um app de desenvolvedor.
- O segredo fica no seu servidor.
Perguntas frequentes
Páginas relacionadas
Incorporações
Apps (miniapps)
Apps OAuth
Autenticação
SDK para TypeScript
Última atualização
Incorporações
As duas formas de levar o Nodaro ao seu produto: um miniapp que executa um workflow publicado e o viewport de cena 3D sem estado, e quando usar cada uma.
Incorporar o viewport de cena 3D
Incorpore o viewport de cena 3D do Nodaro em um iframe e controle-o com postMessage: handshake, mensagens de estado, eventos de edição, assets e limites.