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
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
| Field | Type | Description |
|---|---|---|
symbol | string | Stock symbol (e.g., "AAPL"), 1-20 chars |
action | string | Signal action (see below) |
accountId | UUID | Target account ID |
quantity | number | Order quantity (positive) |
executeMode | string | immediate or scheduled |
Optional Fields
| Field | Type | Default | Description |
|---|---|---|---|
scheduledProcessAt | ISO datetime | - | Required when executeMode is scheduled |
timezone | string | - | IANA timezone, required when scheduledProcessAt or validUntil is set |
validUntil | ISO datetime | - | Signal expiration (requires timezone) |
quantityType | string | fixed | fixed, percentEquity, dollarAmount |
orderType | string | market | market, limit, stop, stopLimit |
limitPrice | number | - | Required for limit and stopLimit orders |
stopPrice | number | - | Required for stop and stopLimit orders |
timeInForce | string | day | day, gtc, opg, cls, ioc, fok, gtd (gtd requires expiresAt) |
expiresAt | ISO datetime | - | Broker order expiry. Required when timeInForce is gtd, within 90 days |
extendedHours | boolean | false | Allow pre/post-market trading |
strategyId | UUID | - | Associated strategy |
sourceId | string | - | Idempotency key (scoped per account) |
replaceExisting | boolean | false | Accepted for backwards compatibility. Has no effect. |
metadata | object | - | Custom key-value pairs |
exitTriggerType | string | - | minutesAfterEntry, minutesBeforeClose, atClockTime, or immediate |
exitTriggerMinutes | integer | - | Minutes count (0 to 60). Required for minutesAfterEntry and minutesBeforeClose |
exitTriggerTime | string | - | HH:MM (24-hour), exchange time (US Eastern for US equities). Required for atClockTime |
exitOrderType | string | - | market, limit, stop, stopLimit, or moc |
exitLimitPrice | number | - | Limit price for the exit order. Required for limit and stopLimit |
exitTimeInForce | string | - | day or cls. Required as cls when exitOrderType is moc |
Signal Actions
| Action | Description |
|---|---|
openLong | Open a long position (buy) |
openShort | Open a short position (sell short) |
closeLong | Close a long position (sell) |
closeShort | Close a short position (buy to cover) |
Quantity Types
| Type | Description | Example |
|---|---|---|
fixed | Exact number of shares | 100 = 100 shares |
percentEquity | Percentage of account equity | 10 = 10% of equity |
dollarAmount | Dollar value to invest | 5000 = $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
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
cancelledorerror
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 is a no-op
The request body still accepts a replaceExisting field for backwards compatibility, but it has no effect. To replace a signal that has not been sent yet, cancel it, then submit the new request. Only an unclaimed queued signal can be cancelled. Once a signal has been sent to the broker, manage its order in the UTM app.
Recommended pattern
- Generate a deterministic
sourceIdfrom the inputs that define the signal. - Always send it.
- On a network error, retry the same request. The dedupe makes it safe.
- To supersede a signal, cancel it, then submit a fresh request with a new
sourceId. - After a fill, never reuse the
sourceIdto force a new attempt. Mint a new one.
Rate Limits
Webhook requests are rate limited:
| Limit | Window |
|---|---|
| 100 requests | per minute |
See Rate Limits for details.
Error Codes
| Code | Description |
|---|---|
INVALID_API_KEY | API key is missing or invalid |
VALIDATION_ERROR | Request payload validation failed |
ACCOUNT_NOT_FOUND | Target account doesn't exist |
RATE_LIMIT_EXCEEDED | Too many requests |
BROKER_ERROR | Broker rejected the order |
Testing
Use paper trading accounts to test your webhook integration:
- Create a paper trading account in UTM
- Get the account ID from the Accounts page
- Send test signals with paper mode enabled
- Verify signals appear in the Signals page
Best Practices
- Use HTTPS - Always send webhooks over HTTPS
- Handle errors - Check response codes and retry on failures
- Validate locally - Validate signal data before sending
- Log requests - Keep logs for debugging
- Use idempotency - Send a stable
sourceIdso retries are safe. See Idempotency and retries - Test first - Use paper trading before live trading