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_IDYou 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 totrueto receive Server-Sent Events instead of one JSON response.selectedOutputs— an array ofblockName.pathstrings, 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/logsRequired Parameters:
workspaceId- Your workspace ID
Optional Filters:
workflowIds- Comma-separated workflow IDsfolderIds- Comma-separated folder IDstriggers- Comma-separated trigger types:api,webhook,schedule,manual,chatlevel- Filter by level:info,errorstartDate- ISO timestamp for date range startendDate- ISO timestamp for date range endexecutionId- Exact execution ID matchminDurationMs- Minimum execution duration in millisecondsmaxDurationMs- Maximum execution duration in millisecondsminCost- Minimum execution costmaxCost- Maximum execution costmodel- Filter by AI model usedmonitorId- Exact monitor IDlisting- JSON-encoded canonical identity withlisting_id,base_id,quote_id, andlisting_type(default,crypto, orcurrency)indicatorId- Exact indicator IDproviderId- Exact market or broker provider IDinterval- Exact monitor intervaltriggerSource- Comma-separated monitor trigger IDs:indicator_trigger,portfolio_state_trigger
Pagination:
limit- Results per page (default: 100)cursor- Cursor for next pageorder- Sort order:desc,asc(default: desc)
Detail Level:
details- Response detail level:basic,full(default: basic)includeTraceSpans- Include trace spans whendetails=full(default: false)includeFinalOutput- Include final output whendetails=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 URLsecret: Optional secret for HMAC signature verificationincludeFinalOutput: Include the workflow's final output in the payloadincludeTraceSpans: Include detailed execution trace spansincludeRateLimits: Include the workflow owner's rate limit informationincludeUsageData: Include the workflow owner's usage and billing datalevelFilter: 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 (alwaysworkflow.execution.completed)tradinggoose-timestamp: Unix timestamp in millisecondstradinggoose-delivery-id: Unique delivery ID for idempotencytradinggoose-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 '', 200Retry 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
-
Polling Strategy: When polling for logs, use cursor-based pagination with
order=ascandstartDateto fetch new logs efficiently. -
Webhook Security: Always configure a webhook secret and verify signatures to ensure requests are from TradingGoose.
-
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. -
Privacy: By default,
finalOutputandtraceSpansare excluded from responses. Only enable these if you need the data and understand the privacy implications. -
Rate Limiting: Implement exponential backoff when you receive 429 responses. Check the
Retry-Afterheader 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 windowX-RateLimit-Remaining: Requests remaining in current windowX-RateLimit-Reset: ISO timestamp when the window resets