Signal Ingest
Submit trading signals to UTM for execution.
POST /api/signals/ingest
Authentication
Requires an API key with signals:write scope in the X-API-Key header.
curl -X POST https://api.example.com/api/signals/ingest \
-H "Content-Type: application/json" \
-H "X-API-Key: your-api-key" \
-d '{"symbol": "AAPL", "action": "openLong", ...}'
Request Body
Required Fields
| Field | Type | Description |
|---|---|---|
symbol | string | Trading symbol (1-20 chars, auto-uppercased) |
action | enum | Signal action: openLong, closeLong, openShort, closeShort |
accountId | UUID | Target trading account ID |
quantity | number | Order quantity (must be positive) |
processAt | string | "now" for immediate, or ISO 8601 datetime for scheduled |
Optional Fields
| Field | Type | Default | Description |
|---|---|---|---|
strategyId | UUID | - | Associated strategy ID |
quantityType | enum | "fixed" | fixed, percentEquity, dollarAmount |
orderType | enum | "market" | market, limit, stop, stop_limit |
limitPrice | number | - | Required for limit and stop_limit orders |
stopPrice | number | - | Required for stop and stop_limit orders |
timeInForce | enum | "day" | day, gtc, opg, cls, ioc, fok, gtd |
extendedHours | boolean | false | Allow pre/post-market trading |
timezone | string | - | IANA timezone (required when processAt is datetime) |
expiresAt | ISO datetime | - | Signal expiration (requires timezone) |
sourceId | string | - | Idempotency key (max 255 chars, scoped per account) |
replace | boolean | false | Replace existing signal with same sourceId |
metadata | object | - | Custom key-value pairs |
exitRule | object | - | Exit rule configuration |
Exit Rules
Automatically create an exit signal when the entry order fills.
Market on Close (MOC)
Place an exit order that executes at market close.
Immediate MOC - Order placed immediately, executes at close:
{
"exitRule": {
"trigger": "market_close",
"immediate": true
}
}
Scheduled MOC - Order placed N minutes before close:
{
"exitRule": {
"trigger": "market_close",
"minutesBefore": 5
}
}
| Field | Type | Default | Description |
|---|---|---|---|
trigger | enum | - | market_close or time |
immediate | boolean | false | Place MOC order immediately on fill |
minutesBefore | number | 0 | Minutes before close (0-30, ignored if immediate: true) |
timeInForce | enum | "cls" | day or cls |
Time-Based Exit
Exit after a duration or at a specific time.
Duration after fill:
{
"exitRule": {
"trigger": "time",
"durationMinutes": 60
}
}
Specific time:
{
"exitRule": {
"trigger": "time",
"exitTime": "15:30"
}
}
| Field | Type | Description |
|---|---|---|
trigger | enum | Must be time |
durationMinutes | number | Minutes after fill to exit |
exitTime | string | Specific time in HH:MM format |
note
For time trigger, provide either durationMinutes or exitTime, not both.
Examples
Minimal Market Order
{
"symbol": "AAPL",
"action": "openLong",
"accountId": "550e8400-e29b-41d4-a716-446655440000",
"quantity": 100,
"processAt": "now"
}
Limit Order
{
"symbol": "TSLA",
"action": "openLong",
"accountId": "550e8400-e29b-41d4-a716-446655440000",
"quantity": 50,
"orderType": "limit",
"limitPrice": 250.0,
"processAt": "now"
}
Scheduled Signal with Immediate MOC Exit
{
"symbol": "MSFT",
"action": "openLong",
"accountId": "550e8400-e29b-41d4-a716-446655440000",
"strategyId": "550e8400-e29b-41d4-a716-446655440001",
"quantity": 75,
"processAt": "2026-02-07T09:25:00",
"timezone": "America/New_York",
"sourceId": "my-strategy-signal-001",
"exitRule": {
"trigger": "market_close",
"immediate": true
}
}
Stop-Limit Order with Replace
{
"symbol": "GOOGL",
"action": "openShort",
"accountId": "550e8400-e29b-41d4-a716-446655440000",
"quantity": 25,
"orderType": "stop_limit",
"stopPrice": 145.0,
"limitPrice": 144.0,
"processAt": "now",
"sourceId": "my-ref-456",
"replace": true
}
Dollar Amount Position Sizing
{
"symbol": "SPY",
"action": "openLong",
"accountId": "550e8400-e29b-41d4-a716-446655440000",
"quantity": 5000,
"quantityType": "dollarAmount",
"processAt": "now"
}
Response
Success - Immediate (201)
{
"message": "Signal processed successfully",
"signal": {
"id": "550e8400-e29b-41d4-a716-446655440099",
"status": "executed",
"orderId": "550e8400-e29b-41d4-a716-446655440098"
}
}
Success - Scheduled (202)
{
"message": "Signal scheduled for processing",
"signal": {
"id": "550e8400-e29b-41d4-a716-446655440099",
"status": "scheduled",
"scheduledAt": "2026-02-07T14:25:00.000Z"
}
}
Validation Error (400)
{
"error": "Validation failed",
"code": "VALIDATION_ERROR",
"details": [
{
"code": "custom",
"message": "limitPrice is required for limit orders",
"path": ["limitPrice"]
}
]
}
Duplicate Signal (400)
{
"error": "Signal rejected",
"message": "Signal with sourceId 'my-ref' already exists with status 'PENDING'. Use replace=true to supersede.",
"signal": {
"id": "550e8400-e29b-41d4-a716-446655440099",
"status": "pending"
},
"code": "SIGNAL_REJECTED"
}
Response Codes
| Code | Description |
|---|---|
201 | Signal executed immediately |
202 | Signal scheduled for future processing |
400 | Validation error or signal rejected |
401 | Missing or invalid API key |
404 | Account or strategy not found |
503 | Service disabled |
Idempotency
Use sourceId to prevent duplicate signals:
- If a signal with the same
sourceIdexists (non-rejected), the existing signal is returned - With
replace: true, the existing signal is cancelled and a new one created - Same
sourceIdcan exist on different accounts (scoped per account) - Only
rejectedsignals allow retry with the samesourceId