Skip to main content

Webhooks

Send trading signals to UTM via webhooks from any source.

Overview

Webhooks allow external systems to send trading signals to UTM. This is useful for:

  • Custom trading bots
  • Third-party signal providers
  • Automated scripts
  • Any system that can make HTTP requests

Webhook URL

Send POST requests to:

https://api.universaltrademanager.com/api/v1/signals

Authentication

Include your API key in the request header:

X-API-Key: your-api-key-here

Or as a query parameter:

?apiKey=your-api-key-here
tip

Use the header method for better security. Query parameters may be logged.

Request Format

Immediate Signal

Process and place the order straight away:

POST /api/v1/signals
Content-Type: application/json
X-API-Key: your-api-key

{
"symbol": "AAPL",
"action": "openLong",
"accountId": "your-account-id",
"quantity": 100,
"executeMode": "immediate"
}

Scheduled Signal

Queue the signal for processing at a specific time. Both scheduledProcessAt (ISO datetime, no Z suffix) and timezone (IANA identifier) are required, and the time must be in the future.

POST /api/v1/signals
Content-Type: application/json
X-API-Key: your-api-key

{
"symbol": "AAPL",
"action": "openLong",
"accountId": "your-account-id",
"quantity": 100,
"executeMode": "scheduled",
"scheduledProcessAt": "2026-05-15T09:30:00",
"timezone": "America/New_York"
}

A scheduled signal returns 202 Accepted with status: "queued" and scheduledProcessAt set; the queue worker processes it at the requested time.

Limit Order with Timed Exit

POST /api/v1/signals
Content-Type: application/json
X-API-Key: your-api-key

{
"symbol": "AAPL",
"action": "openLong",
"accountId": "your-account-id",
"quantity": 100,
"executeMode": "immediate",
"orderType": "limit",
"limitPrice": 150.00,
"exitTriggerType": "minutesAfterEntry",
"exitTriggerMinutes": 30,
"exitOrderType": "market"
}

Request Fields

Required Fields

FieldTypeDescription
symbolstringStock symbol (e.g., "AAPL"), 1-20 chars
actionstringSignal action (see below)
accountIdUUIDTarget account ID
quantitynumberOrder quantity (positive)
executeModestringimmediate or scheduled

Optional Fields

FieldTypeDefaultDescription
scheduledProcessAtISO datetime-Required when executeMode is scheduled
timezonestring-IANA timezone, required when scheduledProcessAt or validUntil is set
validUntilISO datetime-Signal expiration (requires timezone)
quantityTypestringfixedfixed, percentEquity, dollarAmount
orderTypestringmarketmarket, limit, stop, stopLimit
limitPricenumber-Required for limit and stopLimit orders
stopPricenumber-Required for stop and stopLimit orders
timeInForcestringdayday, gtc, opg, cls, ioc, fok, gtd (gtd requires expiresAt)
expiresAtISO datetime-Broker order expiry. Required when timeInForce is gtd, within 90 days
extendedHoursbooleanfalseAllow pre/post-market trading
strategyIdUUID-Associated strategy
sourceIdstring-Idempotency key (scoped per account)
replaceExistingbooleanfalseReplace existing signal with same sourceId
metadataobject-Custom key-value pairs
exitTriggerTypestring-minutesAfterEntry, minutesBeforeClose, atClockTime, or immediate
exitTriggerMinutesinteger-Minutes count (0 to 60). Required for minutesAfterEntry and minutesBeforeClose
exitTriggerTimestring-HH:MM (24-hour). Required for atClockTime
exitOrderTypestring-market, limit, stop, stopLimit, or moc
exitLimitPricenumber-Limit price for the exit order. Required for limit and stopLimit
exitTimeInForcestring-day or cls. Required as cls when exitOrderType is moc

Signal Actions

ActionDescription
openLongOpen a long position (buy)
openShortOpen a short position (sell short)
closeLongClose a long position (sell)
closeShortClose a short position (buy to cover)

Quantity Types

TypeDescriptionExample
fixedExact number of shares100 = 100 shares
percentEquityPercentage of account equity10 = 10% of equity
dollarAmountDollar value to invest5000 = $5,000 worth

Response Format

Success - Immediate (201)

{
"success": true,
"message": "Signal processed successfully",
"signal": {
"id": "550e8400-e29b-41d4-a716-446655440099",
"status": "transmitted",
"orderId": "550e8400-e29b-41d4-a716-446655440098",
"executeMode": "immediate"
}
}

Success - Scheduled (202)

{
"success": true,
"message": "Signal scheduled for processing",
"signal": {
"id": "550e8400-e29b-41d4-a716-446655440099",
"status": "queued",
"scheduledAt": "2026-05-15T13:30:00.000Z",
"executeMode": "scheduled"
}
}

Error

{
"success": false,
"error": "VALIDATION_ERROR",
"message": "Invalid symbol format",
"details": [
{
"field": "symbol",
"message": "Symbol must be uppercase letters only"
}
]
}

Examples

Python

import requests

url = "https://api.universaltrademanager.com/api/v1/signals"
headers = {
"Content-Type": "application/json",
"X-API-Key": "your-api-key"
}
payload = {
"symbol": "AAPL",
"action": "openLong",
"accountId": "your-account-id",
"quantity": 100,
"executeMode": "immediate"
}

response = requests.post(url, json=payload, headers=headers)
print(response.json())

cURL

curl -X POST https://api.universaltrademanager.com/api/v1/signals \
-H "Content-Type: application/json" \
-H "X-API-Key: your-api-key" \
-d '{
"symbol": "AAPL",
"action": "openLong",
"accountId": "your-account-id",
"quantity": 100,
"executeMode": "immediate"
}'

JavaScript

const response = await fetch(
"https://api.universaltrademanager.com/api/v1/signals",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": "your-api-key",
},
body: JSON.stringify({
symbol: "AAPL",
action: "openLong",
accountId: "your-account-id",
quantity: 100,
executeMode: "immediate",
}),
},
);

const data = await response.json();
console.log(data);

Idempotency and retries

Network calls fail. Cron jobs re-fire. A retry that lands twice as two real signals can double your position. UTM uses an idempotency key called sourceId so a script can re-send the same signal as many times as it needs without creating duplicates.

Why sourceId exists

When you set sourceId on a request, UTM treats it as the canonical name for that logical signal. Any subsequent request from the same account with the same sourceId returns the original signal instead of creating a new one. This makes it safe to:

  • Retry after a network timeout
  • Re-fire a cron job that may have already run
  • Replay a webhook that was delivered twice
caution

If you do not set sourceId, there is no dedupe. Every request creates a new signal. A retried network call will place a second order at the broker.

Dedupe scope

The lookup matches on:

  • Same accountId
  • Same sourceId
  • Status is not cancelled or error

Cancelled and errored statuses are excluded on purpose. If the prior attempt did not result in a live order, a retry with the same sourceId is allowed to create a fresh signal. You do not have to mint a new key just to escape a failed attempt.

Choosing a sourceId

The key needs to be stable for the same logical signal, and unique across logical signals. A random UUID per request defeats the purpose, because each retry would generate a different key.

Good patterns:

  • ${strategy}-${symbol}-${barTimestamp} for bar-driven strategies
  • ${runId}-${signalIndex} for batch jobs
  • ${alertId} if your upstream system already issues stable IDs

Maximum length is 255 characters.

replaceExisting

By default, a duplicate request is a no-op: the existing signal is returned with HTTP 200. Set replaceExisting: true if you want the new request to supersede the prior one. For example, when the same bar produces an updated limit price.

The outcome depends on the state of the broker order:

Prior order stateResult of replaceExisting: true
No order yetPrior signal cancelled, new signal created
Order placed, unfilledOrder cancelled at broker, prior signal cancelled, new signal created
Partial or full fillRequest rejected with "Cannot replace signal: associated order has X/Y filled. Cancel position manually or use a new sourceId"
Broker cancel failsLogged as a warning, prior signal cancelled in UTM, new signal created. Broker side may need manual reconciliation

The design rule: UTM will not silently override a signal that has already moved money. Once a fill exists, you must either let the original ride or place a fresh signal with a new sourceId.

  1. Generate a deterministic sourceId from the inputs that define the signal.
  2. Always send it. Default replaceExisting to false.
  3. On a network error, retry the same request. The dedupe makes it safe.
  4. Use replaceExisting: true only when intentionally superseding an unfilled order.
  5. After a fill, never reuse the sourceId to force a new attempt. Mint a new one.

Rate Limits

Webhook requests are rate limited:

LimitWindow
100 requestsper minute

See Rate Limits for details.

Error Codes

CodeDescription
INVALID_API_KEYAPI key is missing or invalid
VALIDATION_ERRORRequest payload validation failed
ACCOUNT_NOT_FOUNDTarget account doesn't exist
MODE_MISMATCHTrading mode doesn't match account type
RATE_LIMIT_EXCEEDEDToo many requests
BROKER_ERRORBroker rejected the order

Testing

Use paper trading accounts to test your webhook integration:

  1. Create a paper trading account in UTM
  2. Get the account ID from the Accounts page
  3. Send test signals with paper mode enabled
  4. Verify signals appear in the Signals page

Best Practices

  1. Use HTTPS - Always send webhooks over HTTPS
  2. Handle errors - Check response codes and retry on failures
  3. Validate locally - Validate signal data before sending
  4. Log requests - Keep logs for debugging
  5. Use idempotency - Send a stable sourceId so retries are safe. See Idempotency and retries
  6. Test first - Use paper trading before live trading