External API

Use the external API to execute deployed workflows, query execution logs, and receive completion webhooks.

Authentication

Workflow execution and Logs API requests require an API key passed in the X-API-Key header:

curl -H "x-api-key: YOUR_API_KEY" \
  https://www.tradinggoose.ai/api/v1/logs?workspaceId=YOUR_WORKSPACE_ID

You can generate API keys from your user settings in the TradingGoose dashboard.

Execute a workflow

Send a POST request to a deployed workflow. The input object is delivered to its API trigger.

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

The request body accepts:

  • input — an object containing the workflow input.
  • stream — set to true to receive Server-Sent Events instead of one JSON response.
  • selectedOutputs — an array of blockName.path strings, such as "Summarize.content", used to select streamed block outputs. Block names must resolve to exactly one deployed block.

Internal execution controls and draft-state overrides are rejected. The endpoint always runs the deployed workflow state.

Without a Response block, a non-streaming request returns the public execution result:

{
  "success": true,
  "output": {
    "content": "..."
  },
  "metadata": {
    "duration": 842,
    "startTime": "2026-09-03T18:30:00.000Z",
    "endTime": "2026-09-03T18:30:00.842Z"
  }
}

If the workflow contains a Response block, a successful execution instead uses that block's configured JSON body, HTTP status, and headers. Non-streaming requests wait up to 25 seconds for the queued execution; a timeout returns HTTP 504.

With stream: true, the response has content type text/event-stream and an X-Execution-Id header. SSE event payloads are JSON, except for the literal [DONE] end marker:

data: {"blockId":"summarize","chunk":"Partial output"}

data: {"event":"final","data":{"success":true,"output":{"content":"..."},"metadata":{"duration":842}}}

data: [DONE]

Check whether the data field equals [DONE] before calling JSON.parse; if it does, stop reading without parsing the marker.

Errors use the same envelope with "event":"error" and an error message.

Logs API

Every Logs API response includes information about your workflow execution limits and usage:

{
  "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
    }
  }
}

Note: The rate limits in the response body are for workflow executions. The rate limits for calling this API endpoint are in the response headers (X-RateLimit-*).

Query Logs

Query workflow execution logs with extensive filtering options.

GET /api/v1/logs

Required Parameters:

  • workspaceId - Your workspace ID

Optional Filters:

  • workflowIds - Comma-separated workflow IDs
  • folderIds - Comma-separated folder IDs
  • triggers - Comma-separated trigger types: api, webhook, schedule, manual, chat
  • level - Filter by level: info, error
  • startDate - ISO timestamp for date range start
  • endDate - ISO timestamp for date range end
  • executionId - Exact execution ID match
  • minDurationMs - Minimum execution duration in milliseconds
  • maxDurationMs - Maximum execution duration in milliseconds
  • minCost - Minimum execution cost
  • maxCost - Maximum execution cost
  • model - Filter by AI model used
  • monitorId - Exact monitor ID
  • listing - JSON-encoded canonical identity with listing_id, base_id, quote_id, and listing_type (default, crypto, or currency)
  • indicatorId - Exact indicator ID
  • providerId - Exact market or broker provider ID
  • interval - Exact monitor interval
  • triggerSource - Comma-separated monitor trigger IDs: indicator_trigger, portfolio_state_trigger

Pagination:

  • limit - Results per page (default: 100)
  • cursor - Cursor for next page
  • order - Sort order: desc, asc (default: desc)

Detail Level:

  • details - Response detail level: basic, full (default: basic)
  • includeTraceSpans - Include trace spans when details=full (default: false)
  • includeFinalOutput - Include final output when details=full (default: false)

With details=full, failure details appear in errorMessage only when either include option is enabled.

{
  "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
    }
  }
}

Get Log Details

Retrieve detailed information about a specific log entry.

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
      }
    }
}

Get Execution Details

Retrieve execution details including the workflow state snapshot.

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
    }
  }
}

Webhook Subscriptions

Get real-time notifications when workflow executions complete.

Subscription options

Webhook-subscription management is not part of the API-key Logs API. The application uses session-authenticated workflow routes to manage subscriptions. Once a workflow has an active subscription, the following options control delivery:

Available Configuration Options:

  • url: Your webhook endpoint URL
  • secret: Optional secret for HMAC signature verification
  • includeFinalOutput: Include the workflow's final output in the payload
  • includeTraceSpans: Include detailed execution trace spans
  • includeRateLimits: Include the workflow owner's rate limit information
  • includeUsageData: Include the workflow owner's usage and billing data
  • levelFilter: Array of log levels to receive (info, error)
  • triggerFilter: Array of trigger types to receive (api, webhook, schedule, manual, chat)
  • active: Whether a newly created subscription starts active (default: true)

Webhook Payload

Failure details appear in data.errorMessage only when includeFinalOutput or includeTraceSpans is enabled.

When a workflow execution completes, TradingGoose sends a POST request to your webhook URL:

{
  "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,
    "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"
  }
}

Webhook Headers

Each webhook request includes these headers:

  • tradinggoose-event: Event type (always workflow.execution.completed)
  • tradinggoose-timestamp: Unix timestamp in milliseconds
  • tradinggoose-delivery-id: Unique delivery ID for idempotency
  • tradinggoose-signature: HMAC-SHA256 signature for verification (if secret configured)
  • Idempotency-Key: Same as delivery ID for duplicate detection

Signature Verification and Duplicate Delivery Handling

Configure a webhook secret and verify the signature before recording or processing a delivery. Retries keep the same Idempotency-Key and tradinggoose-delivery-id, but generate a new payload id, timestamp, and signature. Do not deduplicate using event.id or a body hash. Return 200 for an already completed delivery without repeating its effects.

These examples use a persistent SQLite database and an atomic insert to claim each delivery. Apply database side effects in the same transaction as the receipt: a failure rolls back both, and a retry can try again. The unique constraint also covers the signed event type, workflow ID, and execution ID, because the delivery headers are not included in the signature. This treats each workflow completion as one business event per receiver; use a separate database or consumer scope for subscriptions that need independent processing.

Replace the marked comment with synchronous database writes using that transaction's db; do not start unawaited work.

The Node.js example requires Node.js 24+ with built-in node:sqlite; Python uses its built-in sqlite3 module. Install Express or Flask respectively, and set WEBHOOK_SECRET. Set WEBHOOK_DB_PATH to a persistent database file (default: webhooks.sqlite). Mount this route before any JSON middleware so signature verification receives the original request body.

All receiver workers must use the same durable receipt store. For replicas on different hosts, use a shared transactional database rather than separate SQLite files. Keep receipts for your retry and replay retention period. External effects such as payments or email cannot be rolled back by this database transaction: use the downstream service's idempotency mechanism or a transactional outbox before acknowledging the delivery.

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 '', 200

Retry Policy

Retryable webhook deliveries make up to five total attempts: the initial request and four retries with backoff and jitter.

  • Retry delays: 5 seconds, 15 seconds, 1 minute, and 3 minutes
  • Jitter: Up to 10% additional delay to prevent thundering herd
  • HTTP 5xx, HTTP 429, network errors, and timeouts trigger retries
  • Deliveries timeout after 30 seconds

Webhook deliveries are processed asynchronously and don't affect workflow execution performance.

Best Practices

  1. Polling Strategy: When polling for logs, use cursor-based pagination with order=asc and startDate to fetch new logs efficiently.

  2. Webhook Security: Always configure a webhook secret and verify signatures to ensure requests are from TradingGoose.

  3. Idempotency: Atomically record the delivery ID and database effects in the same durable transaction. Acknowledge completed duplicates with 200; roll back failed processing so retries can succeed. Use downstream idempotency or an outbox for external effects.

  4. Privacy: By default, finalOutput and traceSpans are excluded from responses. Only enable these if you need the data and understand the privacy implications.

  5. Rate Limiting: Implement exponential backoff when you receive 429 responses. Check the Retry-After header for the recommended wait time.

Rate Limiting

The API applies the API endpoint limit configured by the caller's current billing tier. This is separate from the synchronous and asynchronous workflow execution limits included in log responses.

Rate limit information is included in response headers:

  • X-RateLimit-Limit: Maximum requests per window
  • X-RateLimit-Remaining: Requests remaining in current window
  • X-RateLimit-Reset: ISO timestamp when the window resets