API externa
Usa la API externa para ejecutar flujos de trabajo desplegados, consultar registros de ejecución y recibir webhooks de finalización.
Autenticación
La ejecución de flujos de trabajo y las solicitudes a la API de registros requieren una clave de API pasada en el encabezado X-API-Key:
curl -H "x-api-key: YOUR_API_KEY" \
https://www.tradinggoose.ai/api/v1/logs?workspaceId=YOUR_WORKSPACE_IDPuedes generar claves de API desde la configuración de tu usuario en el panel de TradingGoose.
Ejecutar un flujo de trabajo
Envía una solicitud POST a un flujo de trabajo desplegado. El objeto input se entrega a su disparador de API.
curl -X POST \
https://www.tradinggoose.ai/api/workflows/WORKFLOW_ID/execute \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-d '{
"input": {
"topic": "Semiconductor earnings"
}
}'El cuerpo de la solicitud acepta:
input— un objeto que contiene la entrada del flujo de trabajo.stream— establecido entruepara recibir Server-Sent Events en lugar de una respuesta JSON.selectedOutputs— una matriz de cadenasblockName.path, como"Summarize.content", utilizada para seleccionar salidas de bloques transmitidos. Los nombres de los bloques deben resolverse a exactamente un bloque desplegado.
Los controles de ejecución internos y las anulaciones del estado de borrador se rechazan. El endpoint siempre ejecuta el estado desplegado del flujo de trabajo.
Sin un bloque Response, una solicitud sin streaming devuelve el resultado público de la ejecución:
{
"success": true,
"output": {
"content": "..."
},
"metadata": {
"duration": 842,
"startTime": "2026-09-03T18:30:00.000Z",
"endTime": "2026-09-03T18:30:00.842Z"
}
}Si el flujo de trabajo contiene un bloque Response, una ejecución exitosa en su lugar utiliza el cuerpo JSON, el estado HTTP y los encabezados configurados de ese bloque. Las solicitudes sin streaming esperan hasta 25 segundos por la ejecución en cola; un tiempo de espera agotado devuelve HTTP 504.
Con stream: true, la respuesta tiene el tipo de contenido text/event-stream y un encabezado X-Execution-Id. Los datos de los eventos SSE son JSON, excepto el marcador de fin literal [DONE]:
data: {"blockId":"summarize","chunk":"Partial output"}
data: {"event":"final","data":{"success":true,"output":{"content":"..."},"metadata":{"duration":842}}}
data: [DONE]Antes de llamar a JSON.parse, comprueba si el campo data es exactamente [DONE]; en ese caso, deja de leer sin intentar analizar el marcador como JSON.
Los errores utilizan el mismo sobre con "event":"error" y un mensaje error.
API de registros
Cada respuesta de la API de registros incluye información sobre los límites de ejecución y el uso de tu flujo de trabajo:
{
"limits": {
"executionRateLimit": {
"sync": {
"limit": 60, // Max sync workflow executions per minute
"remaining": 58, // Remaining sync workflow executions
"resetAt": "..." // When the window resets
},
"async": {
"limit": 60, // Max async workflow executions per minute
"remaining": 59, // Remaining async workflow executions
"resetAt": "..." // When the window resets
}
},
"usage": {
"currentPeriodCost": 1.234, // Current billing period usage in USD
"limit": 10, // Usage limit in USD
"tier": { // Current billing tier summary
"id": "tier_pro",
"displayName": "Pro"
},
"isExceeded": false // Whether limit is exceeded
}
}
}Nota: Los límites de frecuencia del cuerpo de la respuesta corresponden a las ejecuciones de flujos de trabajo. Los límites de frecuencia para llamar a este endpoint de la API están en los encabezados de la respuesta (X-RateLimit-*).
Consultar registros
Consulta los registros de ejecución de flujos de trabajo con amplias opciones de filtrado.
GET /api/v1/logsParámetros requeridos:
workspaceId- El ID de tu espacio de trabajo
Filtros opcionales:
workflowIds- IDs de flujos de trabajo separados por comasfolderIds- IDs de carpetas separados por comastriggers- Tipos de disparador separados por comas:api,webhook,schedule,manual,chatlevel- Filtrar por nivel:info,errorstartDate- Marca de tiempo ISO para el inicio del rango de fechasendDate- Marca de tiempo ISO para el fin del rango de fechasexecutionId- Coincidencia exacta del ID de ejecuciónminDurationMs- Duración mínima de la ejecución en milisegundosmaxDurationMs- Duración máxima de la ejecución en milisegundosminCost- Costo mínimo de ejecuciónmaxCost- Costo máximo de ejecuciónmodel- Filtrar por el modelo de IA utilizadomonitorId- ID exacto del monitorlisting- Identidad canónica codificada en JSON conlisting_id,base_id,quote_idylisting_type(default,cryptoocurrency)indicatorId- ID exacto del indicadorproviderId- ID exacto del proveedor de mercado o brókerinterval- Intervalo exacto del monitortriggerSource- IDs de disparadores del monitor separados por comas:indicator_trigger,portfolio_state_trigger
Paginación:
limit- Resultados por página (predeterminado: 100)cursor- Cursor para la página siguienteorder- Orden de clasificación:desc,asc(predeterminado: desc)
Nivel de detalle:
details- Nivel de detalle de la respuesta:basic,full(predeterminado: basic)includeTraceSpans- Incluir spans de traza cuandodetails=full(predeterminado: false)includeFinalOutput- Incluir la salida final cuandodetails=full(predeterminado: false)
Con details=full, los detalles del error aparecen en errorMessage solo si se activa alguna de estas dos opciones de inclusión.
{
"data": [
{
"id": "log_abc123",
"workflowId": "wf_xyz789",
"executionId": "exec_def456",
"level": "info",
"trigger": "api",
"startedAt": "2025-01-01T12:34:56.789Z",
"endedAt": "2025-01-01T12:34:57.123Z",
"totalDurationMs": 334,
"cost": {
"total": 0.00234
},
"files": null
}
],
"nextCursor": "eyJzIjoiMjAyNS0wMS0wMVQxMjozNDo1Ni43ODlaIiwiaWQiOiJsb2dfYWJjMTIzIn0",
"limits": {
"executionRateLimit": {
"sync": {
"limit": 60,
"remaining": 58,
"resetAt": "2025-01-01T12:35:56.789Z"
},
"async": {
"limit": 60,
"remaining": 59,
"resetAt": "2025-01-01T12:35:56.789Z"
}
},
"usage": {
"currentPeriodCost": 1.234,
"limit": 10,
"tier": {
"id": "tier_pro",
"displayName": "Pro"
},
"isExceeded": false
}
}
}Obtener detalles del log
Recupera información detallada sobre una entrada de log específica.
GET /api/v1/logs/{id}{
"data": {
"id": "log_abc123",
"workflowId": "wf_xyz789",
"executionId": "exec_def456",
"level": "info",
"trigger": "api",
"startedAt": "2025-01-01T12:34:56.789Z",
"endedAt": "2025-01-01T12:34:57.123Z",
"totalDurationMs": 334,
"workflow": {
"id": "wf_xyz789",
"name": "My Workflow",
"description": "Process customer data",
"color": "#2dd4bf",
"folderId": "folder_123",
"userId": "user_123",
"workspaceId": "workspace_123",
"createdAt": "2025-01-01T10:00:00.000Z",
"updatedAt": "2025-01-01T11:00:00.000Z"
},
"executionData": {
"traceSpans": [...],
"finalOutput": {...}
},
"cost": {
"total": 0.00234,
"tokens": {
"prompt": 123,
"completion": 456,
"total": 579
},
"models": {
"gpt-4o": {
"input": 0.001,
"output": 0.00134,
"total": 0.00234,
"tokens": {
"prompt": 123,
"completion": 456,
"total": 579
}
}
}
},
"createdAt": "2025-01-01T12:34:57.200Z"
},
"limits": {
"executionRateLimit": {
"sync": {
"limit": 60,
"remaining": 58,
"resetAt": "2025-01-01T12:35:56.789Z"
},
"async": {
"limit": 60,
"remaining": 59,
"resetAt": "2025-01-01T12:35:56.789Z"
}
},
"usage": {
"currentPeriodCost": 1.234,
"limit": 10,
"tier": {
"id": "tier_pro",
"displayName": "Pro"
},
"isExceeded": false
}
}
}Obtener detalles de la ejecución
Recupera los detalles de la ejecución, incluida la instantánea del estado del flujo de trabajo.
GET /api/v1/logs/executions/{executionId}{
"executionId": "exec_def456",
"workflowId": "wf_xyz789",
"workflowState": {
"blocks": {...},
"edges": [...],
"loops": {...},
"parallels": {...}
},
"executionMetadata": {
"trigger": "api",
"startedAt": "2025-01-01T12:34:56.789Z",
"endedAt": "2025-01-01T12:34:57.123Z",
"totalDurationMs": 334,
"cost": {...}
},
"limits": {
"executionRateLimit": {
"sync": {
"limit": 60,
"remaining": 58,
"resetAt": "2025-01-01T12:35:56.789Z"
},
"async": {
"limit": 60,
"remaining": 59,
"resetAt": "2025-01-01T12:35:56.789Z"
}
},
"usage": {
"currentPeriodCost": 1.234,
"limit": 10,
"tier": {
"id": "tier_pro",
"displayName": "Pro"
},
"isExceeded": false
}
}
}Suscripciones de webhook
Recibe notificaciones en tiempo real cuando se completen las ejecuciones del flujo de trabajo.
Opciones de suscripción
La gestión de suscripciones de webhook no forma parte de la Logs API con clave de API. La aplicación usa rutas de flujo de trabajo autenticadas por sesión para gestionar las suscripciones. Una vez que un flujo de trabajo tiene una suscripción activa, las siguientes opciones controlan la entrega:
Opciones de configuración disponibles:
url: URL de tu endpoint de webhooksecret: Secreto opcional para la verificación de firma HMACincludeFinalOutput: Incluir la salida final del flujo de trabajo en el payloadincludeTraceSpans: Incluir los spans detallados de la traza de ejecuciónincludeRateLimits: Incluir la información de límite de tasa del propietario del flujo de trabajoincludeUsageData: Incluir los datos de uso y facturación del propietario del flujo de trabajolevelFilter: Arreglo de niveles de log que se recibirán (info,error)triggerFilter: Arreglo de tipos de disparador que se recibirán (api,webhook,schedule,manual,chat)active: Si una suscripción recién creada se inicia activa (predeterminado:true)
Payload del webhook
Cuando se completa una ejecución del flujo de trabajo, TradingGoose envía una solicitud POST a tu URL de webhook:
{
"id": "evt_123",
"type": "workflow.execution.completed",
"timestamp": 1735925767890,
"data": {
"workflowId": "wf_xyz789",
"executionId": "exec_def456",
"status": "success",
"level": "info",
"trigger": "api",
"startedAt": "2025-01-01T12:34:56.789Z",
"endedAt": "2025-01-01T12:34:57.123Z",
"totalDurationMs": 334,
"cost": {
"total": 0.00234,
"tokens": {
"prompt": 123,
"completion": 456,
"total": 579
},
"models": {
"gpt-4o": {
"input": 0.001,
"output": 0.00134,
"total": 0.00234,
"tokens": {
"prompt": 123,
"completion": 456,
"total": 579
}
}
}
},
"files": null,
"errorMessage": "...", // Solo en errores, si includeFinalOutput=true o includeTraceSpans=true
"finalOutput": {...}, // Only if includeFinalOutput=true
"traceSpans": [...], // Only if includeTraceSpans=true
"rateLimits": {...}, // Only if includeRateLimits=true
"usage": {...} // Only if includeUsageData=true
},
"links": {
"log": "/v1/logs/log_abc123",
"execution": "/v1/logs/executions/exec_def456"
}
}Encabezados del webhook
Cada solicitud de webhook incluye estos encabezados:
tradinggoose-event: Tipo de evento (siempreworkflow.execution.completed)tradinggoose-timestamp: Marca de tiempo Unix en milisegundostradinggoose-delivery-id: ID de entrega único para la idempotenciatradinggoose-signature: Firma HMAC-SHA256 para la verificación (si se configuró un secreto)Idempotency-Key: Igual que el ID de entrega para la detección de duplicados
Verificación de firma y gestión de entregas duplicadas
Configura un secreto de webhook y verifica la firma antes de registrar o procesar una entrega. Los reintentos conservan el mismo Idempotency-Key y tradinggoose-delivery-id, pero generan un nuevo id del payload, una nueva marca de tiempo y una nueva firma. No dedupliques mediante event.id ni un hash del cuerpo. Devuelve 200 para una entrega ya completada sin repetir sus efectos.
Estos ejemplos usan una base de datos SQLite persistente y una inserción atómica para reclamar cada entrega. Aplica los efectos en la base de datos dentro de la misma transacción que el registro de recepción: si se produce un error, ambos se revierten y el reintento puede volver a procesarse. La restricción de unicidad también abarca el tipo de evento, el ID del flujo de trabajo y el ID de ejecución firmados, porque los encabezados de entrega no están incluidos en la firma. Así, cada finalización de flujo de trabajo se trata como un único evento de negocio por receptor; usa una base de datos o un ámbito de consumidor independiente para las suscripciones que necesiten procesarse por separado.
Sustituye el comentario indicado por escrituras síncronas en la base de datos usando el db de esa transacción; no inicies trabajo sin esperar a que finalice.
El ejemplo de Node.js requiere Node.js 24+ con node:sqlite integrado; Python usa su módulo integrado sqlite3. Instala Express o Flask, respectivamente, y configura WEBHOOK_SECRET. Establece WEBHOOK_DB_PATH en un archivo de base de datos persistente (predeterminado: webhooks.sqlite). Monta esta ruta antes de cualquier middleware JSON para que la verificación de firma reciba el cuerpo original de la solicitud.
Todos los procesos del receptor deben usar el mismo almacén duradero de registros de recepción. Para réplicas en distintos hosts, usa una base de datos transaccional compartida en lugar de archivos SQLite separados. Conserva los registros durante el período de retención de reintentos y repeticiones. Los efectos externos, como pagos o correos electrónicos, no pueden revertirse mediante esta transacción de base de datos: usa el mecanismo de idempotencia del servicio de destino o una bandeja de salida transaccional antes de confirmar la entrega.
import crypto from 'crypto';
import { DatabaseSync } from 'node:sqlite';
import express from 'express';
const app = express();
const db = new DatabaseSync(process.env.WEBHOOK_DB_PATH || 'webhooks.sqlite', { timeout: 5000 });
db.exec(`
CREATE TABLE IF NOT EXISTS webhook_receipts (
delivery_id TEXT PRIMARY KEY,
event_type TEXT NOT NULL,
workflow_id TEXT NOT NULL,
execution_id TEXT NOT NULL,
UNIQUE (event_type, workflow_id, execution_id)
)
`);
const claimDelivery = db.prepare(`
INSERT INTO webhook_receipts (delivery_id, event_type, workflow_id, execution_id)
VALUES (?, ?, ?, ?) ON CONFLICT DO NOTHING
`);
function verifyWebhookSignature(rawBody, signature, secret) {
if (!signature || !secret) return false;
const [timestampPart, signaturePart] = signature.split(',');
if (!timestampPart || !signaturePart) return false;
const timestamp = timestampPart.replace('t=', '');
const expectedSignature = signaturePart.replace('v1=', '');
const computedSignature = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
const computed = Buffer.from(computedSignature, 'hex');
const expected = Buffer.from(expectedSignature, 'hex');
return computed.length === expected.length && crypto.timingSafeEqual(computed, expected);
}
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['tradinggoose-signature'];
const rawBody = req.body.toString('utf8');
if (!verifyWebhookSignature(rawBody, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const deliveryHeader = req.headers['tradinggoose-delivery-id'];
const deliveryId = req.headers['idempotency-key'] || deliveryHeader;
if (typeof deliveryId !== 'string' || !deliveryId.trim() ||
(deliveryHeader && deliveryHeader !== deliveryId)) {
return res.status(400).send('Missing or inconsistent delivery ID');
}
let event;
try {
event = JSON.parse(rawBody);
} catch {
return res.status(400).send('Invalid JSON');
}
if (event?.type !== 'workflow.execution.completed' ||
typeof event.data?.workflowId !== 'string' || !event.data.workflowId ||
typeof event.data?.executionId !== 'string' || !event.data.executionId) {
return res.status(400).send('Invalid event');
}
try {
db.exec('BEGIN IMMEDIATE');
const receipt = claimDelivery.run(deliveryId, event.type, event.data.workflowId, event.data.executionId);
if (receipt.changes === 0) {
db.exec('COMMIT');
return res.sendStatus(200);
}
// Apply your database side effects here using db, inside this transaction.
db.exec('COMMIT');
} catch (error) {
if (db.isTransaction) db.exec('ROLLBACK');
console.error('Webhook processing failed', error);
return res.sendStatus(500);
}
res.sendStatus(200);
});import hmac
import hashlib
import os
import sqlite3
from contextlib import closing
from flask import Flask, request
app = Flask(__name__)
database_path = os.environ.get('WEBHOOK_DB_PATH', 'webhooks.sqlite')
with closing(sqlite3.connect(database_path)) as db, db:
db.execute('''
CREATE TABLE IF NOT EXISTS webhook_receipts (
delivery_id TEXT PRIMARY KEY,
event_type TEXT NOT NULL,
workflow_id TEXT NOT NULL,
execution_id TEXT NOT NULL,
UNIQUE (event_type, workflow_id, execution_id)
)
''')
def verify_webhook_signature(raw_body: str, signature: str, secret: str) -> bool:
if not signature or not secret:
return False
timestamp_part, separator, signature_part = signature.partition(',')
if not separator:
return False
timestamp = timestamp_part.replace('t=', '')
expected_signature = signature_part.replace('v1=', '')
signature_base = f"{timestamp}.{raw_body}"
computed_signature = hmac.new(
secret.encode(),
signature_base.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(computed_signature, expected_signature)
@app.route('/webhook', methods=['POST'])
def webhook():
signature = request.headers.get('tradinggoose-signature')
raw_body = request.get_data(as_text=True)
if not verify_webhook_signature(raw_body, signature, os.environ['WEBHOOK_SECRET']):
return 'Invalid signature', 401
event = request.get_json()
delivery_header = request.headers.get('tradinggoose-delivery-id')
delivery_id = request.headers.get('Idempotency-Key') or delivery_header
if not delivery_id or not delivery_id.strip() or (
delivery_header and delivery_header != delivery_id
):
return 'Missing or inconsistent delivery ID', 400
data = event.get('data') if isinstance(event, dict) else None
if not isinstance(data, dict) or event.get('type') != 'workflow.execution.completed' or any(
not isinstance(data.get(key), str) or not data[key]
for key in ('workflowId', 'executionId')
):
return 'Invalid event', 400
try:
with closing(sqlite3.connect(database_path, timeout=5)) as db, db:
db.execute('BEGIN IMMEDIATE')
receipt = db.execute('''
INSERT INTO webhook_receipts (delivery_id, event_type, workflow_id, execution_id)
VALUES (?, ?, ?, ?) ON CONFLICT DO NOTHING
''', (delivery_id, event['type'], data['workflowId'], data['executionId']))
if receipt.rowcount == 0:
return '', 200
# Apply your database side effects here using db, inside this transaction.
except Exception:
app.logger.exception('Webhook processing failed')
return '', 500
return '', 200Política de reintentos
Las entregas de webhook que se pueden reintentar realizan hasta cinco intentos en total: la solicitud inicial y cuatro reintentos con retroceso y jitter.
- Retrasos de reintento: 5 segundos, 15 segundos, 1 minuto y 3 minutos
- Jitter: hasta un 10 % de retraso adicional para evitar el efecto estampida
- Los errores HTTP 5xx, los errores HTTP 429, los errores de red y los tiempos de espera agotados activan los reintentos
- Las entregas agotan el tiempo de espera a los 30 segundos
Las entregas de webhook se procesan de forma asíncrona y no afectan el rendimiento de la ejecución del flujo de trabajo.
Buenas prácticas
-
Estrategia de sondeo: Al sondear logs, usa la paginación basada en cursor con
order=ascestartDatepara obtener logs nuevos de forma eficiente. -
Seguridad del webhook: Configura siempre un secreto de webhook y verifica las firmas para asegurarte de que las solicitudes provienen de TradingGoose.
-
Idempotencia: Registra atómicamente el ID de entrega y los efectos en la base de datos dentro de la misma transacción duradera. Confirma los duplicados ya completados con
200; revierte el procesamiento fallido para que los reintentos puedan completarse. Usa idempotencia en el servicio de destino o una bandeja de salida transaccional para los efectos externos. -
Privacidad: De forma predeterminada,
finalOutputetraceSpansse excluyen de las respuestas. Actívalos solo si necesitas los datos y entiendes las implicaciones para la privacidad. -
Límite de solicitudes: Implementa un retroceso exponencial cuando recibas respuestas 429. Consulta el encabezado
Retry-Afterpara conocer el tiempo de espera recomendado.
Límite de solicitudes
La API aplica el límite de endpoints de API configurado por el nivel de facturación actual de quien realiza la llamada. Es independiente de los límites de ejecución de flujo de trabajo síncronos y asíncronos incluidos en las respuestas de logs.
La información del límite de solicitudes se incluye en los encabezados de respuesta:
X-RateLimit-Limit: Solicitudes máximas por ventanaX-RateLimit-Remaining: Solicitudes restantes en la ventana actualX-RateLimit-Reset: Marca de tiempo ISO cuando se restablece la ventana