Integração com Node.js
Envie um arquivo ou lote à automação do painel e receba os resultados no seu servidor.
Você vai precisar de uma automação publicada e uma chave com execute e read.
Nesta página
Configure os dados do painel
Copie a chave autorizada, o ID e a versão publicada da automação. Configure as quatro variáveis no ambiente do servidor.
DUCKFILE_API_URL=https://duckfile.com/api/v1/externalDUCKFILE_API_KEY=SUA_CHAVE_NO_SERVIDORDUCKFILE_AUTOMATION_ID=UUID_DA_AUTOMACAODUCKFILE_AUTOMATION_VERSION=1Use HTTPS com certificado válido e mantenha a chave somente no backend.
Envie e receba os arquivos
Baixe o arquivo de integração, incorpore ao seu projeto e chame a função. Ela envia, acompanha e recebe cada resultado individualmente. Aceita um caminho ou uma lista de caminhos.
import { executarAutomacao } from './duckfile.mjs'; const resultado = await executarAutomacao('imagem.png');console.log(resultado.status, resultado.arquivos);Guarde os arquivos recebidos no seu próprio sistema. A execução não salva nada na biblioteca da conta.
Use os resultados no seu negócio

A função retorna o ID, o estado e os caminhos locais em arquivos. Em resultado parcial, preserve os itens recebidos e trate as falhas separadamente. A função só termina após baixar os resultados disponíveis.
{"id":"UUID_DA_EXECUCAO","status":"COMPLETED","arquivos":[{"id":"UUID_DO_ARQUIVO","path":"resultado-UUID.webp","bytes":12345}],"falhas":[]}Código da integração

Fonte de exemplo, sem SDK oficial. Você pode incorporar e adaptar a implementação, incluindo persistir a chave de idempotência e o ID da execução para recuperar falhas de rede. Nenhuma etapa da automação é definida nesse código.
import { randomUUID } from 'node:crypto';import { readFile, mkdir, link, rm } from 'node:fs/promises';import { createWriteStream } from 'node:fs';import { Transform, Readable } from 'node:stream';import { pipeline } from 'node:stream/promises';import { basename, extname, join } from 'node:path';import { setTimeout as pause } from 'node:timers/promises';import { realpathSync } from 'node:fs';import { fileURLToPath } from 'node:url'; // Exemplo para incorporar ao seu servidor; não é um SDK oficial.export async function executarAutomacao(entrada, { idempotencyKey = randomUUID(), outputDir = '.' } = {}) { if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0') throw new Error('Mantenha a validação TLS habilitada.'); const base = new URL(process.env.DUCKFILE_API_URL); if (base.protocol !== 'https:' || base.username || base.password || base.search || base.hash || !base.pathname.replace(/\/$/, '').endsWith('/api/v1/external')) throw new Error('Configure a base HTTPS da API.'); const api = base.href.replace(/\/$/, ''); const key = process.env.DUCKFILE_API_KEY; const automation = process.env.DUCKFILE_AUTOMATION_ID; const version = Number(process.env.DUCKFILE_AUTOMATION_VERSION); const uuid = /^[a-f\d]{8}-(?:[a-f\d]{4}-){3}[a-f\d]{12}$/i; if (!key || /[\r\n]/.test(key) || !uuid.test(automation ?? '') || !Number.isSafeInteger(version) || version < 1) throw new Error('Configure chave, ID e versão publicados no painel.'); const paths = Array.isArray(entrada) ? entrada : [entrada]; if (!paths.length || paths.length > 100) throw new Error('Informe um arquivo ou lote.'); const types = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.webp': 'image/webp', '.avif': 'image/avif' }; const form = new FormData(); form.set('version', String(version)); let total = 0; for (const path of paths) { const type = types[extname(path).toLowerCase()], bytes = await readFile(path); total += bytes.length; if (!type || !bytes.length || bytes.length > 25000000 || total > 79000000) throw new Error('Use imagens de até 25 MB e respeite o limite total de 80 MB com o formulário.'); form.append('files', new Blob([bytes], { type }), basename(path)); } async function json(path, body) { let response; try { response = await fetch(api + path, { method: body ? 'POST' : 'GET', headers: { Authorization: 'Bearer ' + key, ...(body ? { 'Idempotency-Key': idempotencyKey } : {}) }, ...(body ? { body } : {}), signal: AbortSignal.timeout(body ? 150000 : 30000), redirect: 'error' }); } catch (error) { error.idempotencyKey = idempotencyKey; throw error; } if (!response.ok) { const problem = await response.json().catch(() => ({})); const error = new Error('API: HTTP ' + response.status + ' ' + (problem.code ?? '') + '. Preserve a chave de idempotência em caso de reenvio.'); error.idempotencyKey = idempotencyKey; throw error; } return (await response.json()).data; } let run = await json('/automations/' + automation + '/runs', form); if (!uuid.test(run.id)) throw new Error('ID de execução inválido.'); const deadline = Date.now() + 35 * 60 * 1000; const terminal = ['COMPLETED', 'PARTIAL', 'FAILED', 'CANCELLED', 'EXPIRED']; while (!terminal.includes(run.status)) { if (Date.now() >= deadline) throw new Error('Prazo local atingido. Acompanhe a execução ' + run.id + ' antes de reenviar.'); await pause(2000); run = await json('/runs/' + run.id); } await mkdir(outputDir, { recursive: true }); const arquivos = []; for (const file of run.files ?? []) { if (!file.result) continue; if (!uuid.test(file.id) || file.result.path !== '/runs/' + run.id + '/files/' + file.id) throw new Error('Caminho de resultado inválido.'); const expected = Number(file.result.bytes); if (!Number.isSafeInteger(expected) || expected < 1 || expected > 100000000) throw new Error('Tamanho de resultado inválido.'); const suffix = { 'image/png': 'png', 'image/jpeg': 'jpg', 'image/webp': 'webp', 'image/avif': 'avif' }[file.result.mimeType]; if (!suffix) throw new Error('Formato de resultado inválido.'); const path = join(outputDir, 'resultado-' + file.id + '.' + suffix); const partial = path + '.' + randomUUID() + '.part'; const response = await fetch(api + file.result.path, { headers: { Authorization: 'Bearer ' + key }, signal: AbortSignal.timeout(120000), redirect: 'error' }); if (!response.ok || !response.body) throw new Error('Recebimento: HTTP ' + response.status); let received = 0; try { await pipeline(Readable.fromWeb(response.body), new Transform({ transform(chunk, encoding, callback) { received += chunk.length; callback(received > expected ? new Error('Tamanho de resultado inválido.') : null, chunk); } }), createWriteStream(partial, { mode: 0o600, flags: 'wx' })); if (received !== expected) throw new Error('Arquivo recebido incompleto.'); await link(partial, path); // Publicação exclusiva: nunca substitui um resultado existente. } finally { await rm(partial, { force: true }); } arquivos.push({ id: file.id, path, bytes: received }); } const falhas = (run.files ?? []).filter(file => file.status !== 'COMPLETED' || file.partial).map(file => ({ id: file.id, errorCode: file.errorCode })); if (!arquivos.length) throw new Error('Nenhum arquivo disponível: ' + run.status); return { id: run.id, status: run.status, idempotencyKey, arquivos, falhas };}if (process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) { console.log(JSON.stringify(await executarAutomacao(process.argv.slice(2)), null, 2));}Trate falhas e reenvios

Não crie outra execução apenas porque a conexão caiu. Preserve a chave de idempotência e os mesmos arquivos. Acompanhe o ID já recebido; mudança de versão, falta de crédito e falha de validação exigem tratamento próprio.