DxB Algo Strategist Webhook API
Authentication
Endpoint
POST https://api.dxbalgo.com/api/v1/webhook/signals
(Sandbox URL and credentials differ; see Sandbox.)
OpenAPI documentation endpoints (/docs, /redoc, /openapi.json) are disabled. This document is the single source of truth for the webhook contract. If you plan to generate a client library, build from this document instead of an OpenAPI schema.
Headers
Every webhook carries three required headers:
X-DXB-Key— Your strategy's API key identifier (e.g.,live_acme-momo-a). This identifies your strategy; the payload never names one. Two strategists may reuse a message ID without colliding because the key scopes idempotency.X-DXB-Timestamp— The current moment in ISO 8601 format with explicit timezone offset (e.g.,2026-08-13T10:45:30+05:30). This header authenticates the request; a timestamp outside ±300 seconds of our server time returns a 401 Unauthorized with nothing recorded (see Response Handling).X-DXB-Signature— HMAC-SHA256 signature proving you hold the API secret. Computed over the raw request body before any parsing or re-serialization. See below.
Signature Computation
The signature verifies that you, not an attacker, composed this request. Compute it as follows:
- Get the raw request body as bytes (the literal string that went on the wire, before any JSON parsing or reformatting).
- Get the value of the
X-DXB-Timestampheader. - Concatenate:
timestamp + "." + raw_body(e.g.,"2026-08-13T10:45:30+05:30." + the_raw_bytes). - HMAC-SHA256 using your API secret as the key.
- Encode the result as hexadecimal.
- Place it in the
X-DXB-Signatureheader.
Critical: Sign the raw bytes of the body, not a re-serialized JSON string. If you parse the JSON, then stringify it again, the bytes will not match and we reject you with a 401. This is the most common integration mistake — test it explicitly before going live.
Python Example
import hashlib
import hmac
from datetime import datetime, timezone
# Your API credentials
api_key = "live_acme-momo-a"
api_secret = "your_secret_here"
# Current moment, with an explicit offset. UTC (+00:00) is perfectly
# acceptable — what matters is that an offset is present at all. This is the
# header timestamp; the `signal_ts` inside your payload is separate and also
# needs an offset.
timestamp = datetime.now(timezone.utc).isoformat() # "2026-08-13T05:15:30+00:00"
# Your instruction as a JSON string. Use this exact string as the body.
raw_body = '{"signal_id": "acme-2026-08-13-0001", "order_ref": "ORD-1", "action": "entry", ...}'
# Signature: HMAC-SHA256(secret, timestamp + "." + raw_body)
message = (timestamp + "." + raw_body).encode('utf-8')
signature = hmac.new(
api_secret.encode('utf-8'),
message,
hashlib.sha256
).hexdigest()
# Send the request
import requests
requests.post(
"https://api.dxbalgo.com/api/v1/webhook/signals",
headers={
"X-DXB-Key": api_key,
"X-DXB-Timestamp": timestamp,
"X-DXB-Signature": signature,
"Content-Type": "application/json",
},
data=raw_body,
)
Node.js Example
const crypto = require('crypto');
// Your API credentials
const apiKey = 'live_acme-momo-a';
const apiSecret = 'your_secret_here';
// Current moment. toISOString() always yields UTC ("...Z"), which we accept —
// what matters is that the zone is explicit, not that it is IST.
const timestamp = new Date().toISOString(); // "2026-08-13T05:15:30.000Z"
// Your instruction as a JSON string. Use this exact string as the body.
const rawBody = JSON.stringify({
signal_id: 'acme-2026-08-13-0001',
order_ref: 'ORD-1',
action: 'entry',
// ... rest of fields
});
// Signature: HMAC-SHA256(secret, timestamp + "." + raw_body)
const message = timestamp + '.' + rawBody;
const signature = crypto
.createHmac('sha256', apiSecret)
.update(message, 'utf-8')
.digest('hex');
// Send the request
const fetch = require('node-fetch');
fetch('https://api.dxbalgo.com/api/v1/webhook/signals', {
method: 'POST',
headers: {
'X-DXB-Key': apiKey,
'X-DXB-Timestamp': timestamp,
'X-DXB-Signature': signature,
'Content-Type': 'application/json',
},
body: rawBody,
});
Signal ID vs. Order Ref: The Critical Distinction
Every instruction carries two separate identifiers. They solve different problems and must not be confused.
signal_id(the message ID) — Unique per MESSAGE you send to us. If you resend the same message (e.g., after a network timeout), use the samesignal_id. We store it with your strategy ID in a unique constraint; a replayed message is a guaranteed no-op. This is your idempotency key and makes retries safe. A new message = new signal_id, always.order_ref(the position handle) — Stable across the LIFETIME of a position. An entry DEFINES the order_ref; your exit, amend, and cancel instructions cite the sameorder_refto reference that position. The entry "opens" the order_ref; subsequent actions target it by name. Critically: an order_ref may be used by only ONE entry. If you send two entries with the sameorder_ref, the second is rejected with a reason telling you to use a new order_ref. This rule holds even after the first position closes — in this release, order_refs are not recycled. If you need to trade the same symbol again, assign a distinct order_ref (e.g., ORD-448, ORD-449).
Worked Example: Entry → Amend → Partial Exit → Final Exit
1. Entry (opens position ORD-448)
signal_id: acme-2026-08-13-0001
order_ref: ORD-448
action: entry
qty: 100
sl: 2800.0
2. Amend (update the recorded stop level for ORD-448; DxB stores it but does not execute it)
signal_id: acme-2026-08-13-0002 ← NEW signal_id (different message)
order_ref: ORD-448 ← SAME order_ref (same position)
action: amend
sl: 2790.0
3. Partial Exit (close 40 shares of ORD-448)
signal_id: acme-2026-08-13-0003 ← NEW signal_id
order_ref: ORD-448 ← SAME order_ref
action: exit
qty: 40
4. Final Exit (close remaining 60 shares)
signal_id: acme-2026-08-13-0004 ← NEW signal_id
order_ref: ORD-448 ← SAME order_ref
action: exit
qty: 60
Notice: signal_id changes for every message (making retries safe); order_ref stays constant (tying all actions to one position).
If you replay message #1 (e.g., after a network error), we return {status: "duplicate"} — not an error, but the safe outcome of a retry. If you reuse signal_id in a different message, the second one is silently ignored.
The Four Verbs
DxB trades via exactly four actions: entry, exit, amend, and cancel. Each carries a specific set of fields; sending forbidden fields or omitting required ones triggers a refusal.
Entry: Open a Position
Opens a position in one symbol at one of your specified order types.
Required fields:
action: "entry"order_ref— Your identifier for this position (stable for its lifetime). Must be unique per entry; two entries with the same order_ref are rejected (see Signal ID vs. Order Ref).signal_id— Message ID (unique per webhook call you make).signal_ts— Your timestamp (ISO 8601 with offset; see Timestamps).expires_at— When this instruction expires (ISO 8601 with offset).side: "buy"— Must be "buy" on NSE/BSE. Sells are structurally impossible there (CNC/delivery only; shorting is not available). On NFO/BFO a"sell"entry is allowed; see Options.symbol— Stock symbol (e.g.,"RELIANCE","TCS"). Must be upper-case with no surrounding whitespace. We match this exactly against regulatory restriction lists; normalizing it on your behalf would silently edit your instruction and could bypass a restriction you intend.exchange—"NSE"or"BSE"(other exchanges later). Must be upper-case. Must match the exchange your strategy is configured for; an NSE strategy instructing a NASDAQ entry is refused.qty— Number of shares, sized at your strategy'sbase_capital. Per-customer quantity isqty × multiplier, floored to whole shares.order_type— One of:market,limit,sl_limit,gtt. See the matrix below.
Optional fields:
sl— Stop-loss price (reference only; see Stop-Loss and Targets). We record it for audit and dashboard display, and you may amend it via the sameamendverb as targets. We take no action on it. Perfectly valid to omit this; DxB never invents a stop.targets— Up to three target prices (reference only; see Stop-Loss and Targets). We record them for audit and dashboard display but take no action.
Forbidden fields:
- None. All fields not listed as required or optional are rejected.
Order Type Matrix (Entry and Exit)
| order_type | limit_price | trigger | Purpose |
|---|---|---|---|
market |
Forbidden | Forbidden | Immediate fill at market price. (Entry: buy at market; Exit: sell at market.) |
limit |
Required | Forbidden | Resting limit order; fills at or better than this price. (Entry: buy at limit; Exit: sell at limit.) |
sl_limit |
Required | Required | Day stop-limit: if price touches trigger, place a limit order at limit_price. (Entry: buy on trigger; Exit: sell on trigger.) |
gtt |
Required | Required | GTT (good-till-trigger): a resting broker-side trigger order, valid up to expires_at. If price touches trigger, a limit order is placed at limit_price. Survives DxB downtime. |
Entry Example: Market Order
{
"signal_id": "acme-2026-08-13-0042",
"order_ref": "ORD-448",
"action": "entry",
"side": "buy",
"symbol": "RELIANCE",
"exchange": "NSE",
"order_type": "market",
"qty": 10,
"limit_price": null,
"trigger": null,
"sl": 2840.0,
"targets": [
{"price": 2950.0},
{"price": 3010.0},
{"price": 3080.0}
],
"signal_ts": "2026-08-13T09:21:03+05:30",
"expires_at": "2026-08-13T15:15:00+05:30"
}
Entry Example: GTT Order
{
"signal_id": "acme-2026-08-13-0043",
"order_ref": "ORD-449",
"action": "entry",
"side": "buy",
"symbol": "TCS",
"exchange": "NSE",
"order_type": "gtt",
"qty": 5,
"trigger": 3250.0,
"limit_price": 3255.0,
"sl": 3200.0,
"targets": null,
"signal_ts": "2026-08-13T09:22:00+05:30",
"expires_at": "2026-08-18T15:30:00+05:30"
}
Exit: Close a Position
Closes a position entirely or partially. Executes at the order_type you specify, using the same order-type matrix as entry: market, limit, sl_limit, or GTT. Limiting an exit (selling on a limit price) is a common, safe operation at both Zerodha and Angel; DxB does not force all exits to market.
Required fields:
action: "exit"order_ref— The position handle from the entry.signal_id— New message ID (different from every other message).order_type— One of:market,limit,sl_limit,gtt. Must be supplied; there is no default. See the Order Type Matrix above.qty— Absolute number of shares to close. Not yet enforced: this release does not compareqtyagainst what the position actually holds. An exit for more shares than remain is currently accepted, not rejected and not capped — position tracking does not exist yet, so there is nothing to compare against. Do not rely on us to catch an over-sized exit in this release. A future release will cap at what remains and returnaccepted.
Optional fields:
symbol,exchange— If provided, these are VERIFIED against the entry that opened theorder_ref; a mismatch is refused. This is a cross-check to catch wrong order_ref citations before a position gets closed unintentionally.side— If provided, must be"sell".
One rule governs side on all three closing verbs, and it is worth stating once because it looks like two: side is the side of the order this instruction acts on. An exit creates a sell, so it is "sell". An amend or a cancel targets the entry order that already exists, and that order was a buy, so those are "buy".
We recommend sending symbol, exchange and side on every closing instruction. They cost nothing, and they catch the one mistake that is otherwise invisible to us: citing the wrong order_ref. Without them we would faithfully close a position you never meant to touch.
limit_price,trigger— Required for specificorder_typevalues (see matrix).
Forbidden fields:
sl,targets— An exit IS the close; there is no stop-loss or target price on a closing order.
Exit Example: Market Sell
{
"signal_id": "acme-2026-08-13-0050",
"order_ref": "ORD-448",
"action": "exit",
"symbol": "RELIANCE",
"exchange": "NSE",
"side": "sell",
"order_type": "market",
"qty": 10,
"signal_ts": "2026-08-13T14:30:00+05:30",
"expires_at": "2026-08-13T15:30:00+05:30"
}
Exit Example: Limit Sell
{
"signal_id": "acme-2026-08-13-0051",
"order_ref": "ORD-449",
"action": "exit",
"symbol": "TCS",
"exchange": "NSE",
"side": "sell",
"order_type": "limit",
"limit_price": 3310.0,
"qty": 5,
"signal_ts": "2026-08-13T14:35:00+05:30",
"expires_at": "2026-08-13T15:30:00+05:30"
}
Exit Example: GTT Sell (Trigger on Drop, Sell on Limit)
{
"signal_id": "acme-2026-08-13-0052",
"order_ref": "ORD-450",
"action": "exit",
"symbol": "INFY",
"exchange": "NSE",
"side": "sell",
"order_type": "gtt",
"trigger": 1800.0,
"limit_price": 1795.0,
"qty": 20,
"signal_ts": "2026-08-13T14:40:00+05:30",
"expires_at": "2026-08-18T15:30:00+05:30"
}
Amend: Modify a Position or Working Order
Amends a stop-loss, targets, or parameters of a working unfilled entry.
Required fields:
action: "amend"order_ref— The position to modify.signal_id— New message ID.- At least one of the amendable fields (see below). A field present in the request replaces the old value; absent fields keep their current value.
Amendable fields:
- On a working unfilled entry:
qty,limit_price,trigger,sl. - On an open position (filled entry):
sl,targets.
About sl (stop-loss): Amending a stop-loss is accepted whether the entry is still working or already filled. The amendment updates our recorded value of the stop you intend. Important: this does not place a broker order. We record your stop-loss intent; nothing closes the position but an exit. If you believe we are holding a stop order at the broker, you are mistaken — only an exit can close.
NOT amendable:
expires_at— It is every signal's own time-to-live; it is not a property of the resting order. A strategist who stamps every message with a TTL (which the contract invites) would otherwise silently move a working order's expiry just by amending its price. This creates silent surprises and hidden state, which this system refuses. To change a working order's lifetime, sendcancel+ a freshentrywith a newsignal_idand neworder_ref. That makes the change explicit.
Optional fields:
symbol,exchange,side— If provided, these are VERIFIED against the entry that opened theorder_ref; a mismatch is refused. For amend,sidemust be"buy"(matching the entry). This is a cross-check to catch wrong order_ref citations. We recommend sending these fields.
Forbidden:
- Field removal (setting a field to null to delete it). A correction is
cancel+ newentrywith a newsignal_id.
Amend Example: Adjust the Stop and Targets
{
"signal_id": "acme-2026-08-13-0051",
"order_ref": "ORD-448",
"action": "amend",
"symbol": "RELIANCE",
"exchange": "NSE",
"side": "buy",
"sl": 2790.0,
"targets": [
{"price": 2950.0},
{"price": 3050.0}
],
"signal_ts": "2026-08-13T10:00:00+05:30",
"expires_at": "2026-08-13T15:30:00+05:30"
}
Cancel: Kill a Working Order
Cancels an unfilled entry or GTT. If the order is already filled or closed, this is refused with the reason "already filled — send exit".
Required fields:
action: "cancel"order_ref— The working order to cancel.signal_id— New message ID.
Optional fields:
symbol,exchange,side— If provided, these are VERIFIED against the entry that opened theorder_ref; a mismatch is refused. For cancel,sidemust be"buy"(matching the entry). This is a cross-check to catch wrong order_ref citations. We recommend sending these fields.signal_ts,expires_at— For completeness; required by the schema but not used for cancellation logic.
Cancel Example
{
"signal_id": "acme-2026-08-13-0052",
"order_ref": "ORD-449",
"action": "cancel",
"symbol": "TCS",
"exchange": "NSE",
"side": "buy",
"signal_ts": "2026-08-13T10:15:00+05:30",
"expires_at": "2026-08-13T15:30:00+05:30"
}
Options: NFO and BFO
A strategy configured for NFO or BFO trades option contracts. Today these are NIFTY on NFO and SENSEX on BFO, placed through Zerodha. Everything above applies, with these differences:
symbolis the contract's tradingsymbol, exactly as in the broker's instrument dump (e.g.NIFTY26N0322950CE).sideon an entry may be"buy"or"sell". A"sell"entry opens a short position.productis required on an entry:"NRML"or"MIS". There is no default. It is forbidden onexit,amendandcancel, which use the entry's product.lotsreplacesqtyonentryandexit, andqtyis refused. Each customer's quantity isfloor(lots × multiplier)whole lots, multiplied by that contract's lot size from the instrument dump. Lot size belongs to the contract and can differ between expiries of one underlying. A customer who floors to 0 lots is skipped with a recorded reason.exitplaces an order on the opposite side of the entry: a buy for a short, a sell for a long. Sendsideaccordingly if you send it. An exit is capped at what the book holds, so it can never open a position the other way.amendcannot change the size of a working option order. Cancel it and send a new entry with a neworder_ref.gttis not available on NFO or BFO.
You remain solely responsible for closing every leg. DxB does not watch prices or margin. A short position whose exit never arrives stays open, and a leg the broker rejects for margin is recorded as a rejection for that account. Each leg is its own signal and its own order_ref; legs are not yet grouped, so nothing unwinds a partly filled structure.
{
"signal_id": "condor-2026-11-03-0003",
"order_ref": "CONDOR-1103-SHORT-CE",
"action": "entry",
"side": "sell",
"symbol": "NIFTY26N0322950CE",
"exchange": "NFO",
"product": "MIS",
"order_type": "limit",
"limit_price": 120.0,
"lots": 1,
"signal_ts": "2026-11-03T11:00:00+05:30",
"expires_at": "2026-11-03T11:05:00+05:30"
}
Stop-Loss and Targets
Stop-Loss and Targets: Reference-Only (You Control Them)
Both the sl field and the targets field are recorded for audit and displayed on the dashboard, but we take no action on either. You control both by sending explicit instructions:
- Stop-loss: You realize stop levels by sending an
exitsignal when the position should be closed. DxB does not place a broker-side GTT, does not monitor price, and does not close the position if price touches your stop level. If your system is down, or the market gaps below your intended stop, nothing on our side closes the position — the entry remains open. You are solely responsible for closing losing positions. Send anexitsignal; we will execute it. - Targets: You realize target levels by sending partial
exitsignals — as profit is achieved at each target, you instruct us to close that fraction of the position.
This keeps DxB free of any decision logic ("should we take profit?" / "should we cut losses?") and avoids complex multi-leg order orchestration. The strategist decides when; DxB executes what the strategist decides.
Bolded because it is critical for both: If you send three targets and one stop, you must send four partial or full exit signals to close them all. We will not. A strategist who believes DxB executes stops will find their losing positions are not closed and their capital is not protected. A strategist who believes DxB executes targets will find their positions are not closed and their profits are not taken.
DxB never computes a stop-loss or takes profit. We hold no market data, no bar history, and no risk model. If you send no sl, none is recorded. If you want one recorded for audit, send it explicitly. Either way, you must close the position yourself via an exit signal.
Refusal Reasons: The Integration Checklist
The table below enumerates every reason DxB might reject a signal. Use it as your integration checklist: before going live, verify that your implementation cannot trigger any of these. A few refusals are designed but not yet enforced in this release — marked in the Status column.
| Reason | Cause | What to Do | Status |
|---|---|---|---|
empty external_id — no idempotency key |
signal_id is empty or missing. |
Provide a non-empty string for signal_id. |
Enforced |
signal_id is N characters, limit 128 |
signal_id exceeds 128 characters. |
Use a shorter identifier. | Enforced |
empty order_ref — nothing to reference |
order_ref is empty or missing. |
Provide a non-empty string for order_ref. |
Enforced |
order_ref is N characters, limit 128 |
order_ref exceeds 128 characters. |
Use a shorter identifier. | Enforced |
order_ref ... was already opened by an accepted entry — use a new order_ref |
An earlier accepted entry already owns this order_ref. |
Send a new order_ref for this entry. Order refs are not recycled even after a position closes. |
Enforced |
signal_ts {timestamp} has no timezone |
signal_ts is naive (no +HH:MM offset). |
Add explicit timezone: 2026-08-13T09:21:03+05:30, not 2026-08-13T09:21:03. |
Enforced |
expires_at {timestamp} has no timezone |
expires_at is naive. |
Add explicit timezone offset. | Enforced |
signal_ts ... is Ns in the future, beyond the 300s clock tolerance — resync your clock |
signal_ts is more than 300 seconds ahead of our server time. |
Verify your system clock is synchronized. Resync and resend with a new signal_id. |
Enforced |
qty {value} not positive |
qty is zero, negative, or non-numeric. |
Send a positive integer. | Enforced |
qty {value} is NaN or Infinity |
qty is a non-finite number. |
Verify your quantity calculation does not include division by zero. If using Python, watch for json.dumps emitting non-standard NaN literals by default. |
Enforced |
qty ... outside the storable range |
qty exceeds 2^31-1 (2,147,483,647 shares). |
Use a value within the storable integer range. | Enforced |
limit_price {value} not positive |
limit_price is zero, negative, or non-numeric. |
Send a positive float. | Enforced |
limit_price {value} is NaN or Infinity |
limit_price is a non-finite number. |
Check your price calculation for division by zero or missing data. | Enforced |
trigger {value} not positive |
trigger is zero, negative, or non-numeric. |
Send a positive float. | Enforced |
trigger {value} is NaN or Infinity |
trigger is a non-finite number. |
Check your trigger calculation. | Enforced |
sl {value} not positive |
sl is zero, negative, or non-numeric. |
Send a positive float, or omit it. | Enforced |
sl {value} is NaN or Infinity |
sl is a non-finite number. |
Check your stop-loss calculation. | Enforced |
action {value} not supported |
action is not one of: entry, exit, amend, cancel. |
Use one of the four verbs. | Enforced |
entry requires side |
Entry is missing side. |
Add side: "buy" (sell is not supported). |
Enforced |
entry requires symbol |
Entry is missing symbol. |
Add symbol (e.g., "RELIANCE"). |
Enforced |
symbol must be upper-case |
symbol contains lower-case letters or is not as listed on the exchange. |
Use the exact exchange symbol in upper-case (e.g., "RELIANCE", not "reliance" or " RELIANCE "). We match regulatory restriction lists against what you send; silently normalising it would edit your instruction. |
Enforced |
symbol is N characters, limit 128 |
symbol exceeds 128 characters. |
Use a shorter symbol. | Enforced |
entry requires exchange |
Entry is missing exchange. |
Add exchange: "NSE" or "BSE". |
Enforced |
exchange must be upper-case |
exchange contains lower-case letters. |
Use exact upper-case (e.g., "NSE", not "nse"). |
Enforced |
exchange 'X' is not this strategy's exchange 'Y' |
The strategy is configured for one exchange (e.g., NSE) but you sent a different one (e.g., BSE). | Verify the exchange you sent matches your strategy's configured market. | Enforced |
entry requires order_type |
Entry is missing order_type. |
Add one of: market, limit, sl_limit, gtt. | Enforced |
limit_price not applicable to market order |
Market order has a limit_price. |
Remove limit_price or change order_type. |
Enforced |
trigger not applicable to market order |
Market order has a trigger. |
Remove trigger or change order_type. |
Enforced |
limit entry requires limit_price |
Limit order is missing limit_price. |
Add a positive float. | Enforced |
trigger not applicable to limit order |
Limit order has a trigger. |
Remove trigger or change order_type to sl_limit or gtt. |
Enforced |
sl_limit entry requires trigger |
SL_limit order is missing trigger. |
Add a positive float. | Enforced |
sl_limit entry requires limit_price |
SL_limit order is missing limit_price. |
Add a positive float. | Enforced |
gtt entry requires trigger |
GTT order is missing trigger. |
Add a positive float. | Enforced |
gtt entry requires limit_price |
GTT order is missing limit_price. |
Add a positive float. | Enforced |
entry requires qty |
Entry is missing qty. |
Add a positive integer. | Enforced |
entry requires expires_at |
Entry is missing expires_at. |
Add an ISO 8601 timestamp with offset. | Enforced |
short entry not supported (CNC only) |
Entry has side: "sell". |
Only side: "buy" is allowed (delivery product cannot short). |
Enforced |
more than 3 targets |
targets array has more than three items. |
Remove excess targets (max 3). | Enforced |
each target must be an object with a positive price |
A target is not {price: <positive number>}. |
Ensure each target has a positive price field. |
Enforced |
exit requires qty (absolute) |
Exit is missing qty. |
Provide the absolute number of shares to close. | Enforced |
exit requires order_type |
Exit is missing order_type. |
Add one of: market, limit, sl_limit, gtt. Exits are real orders; there is no default. | Enforced |
sl not applicable to exit |
Exit has an sl field. |
Remove it; an exit IS the close. | Enforced |
targets not applicable to exit |
Exit has a targets field. |
Remove it; an exit IS the close. Realize targets by sending partial exits. | Enforced |
limit_price not applicable to market order (exit) |
Market exit has a limit_price. |
Remove limit_price or change order_type. |
Enforced |
trigger not applicable to market order (exit) |
Market exit has a trigger. |
Remove trigger or change order_type. |
Enforced |
limit exit requires limit_price |
Limit exit is missing limit_price. |
Add a positive float. | Enforced |
trigger not applicable to limit order (exit) |
Limit exit has a trigger. |
Remove trigger or change order_type to sl_limit or gtt. |
Enforced |
sl_limit exit requires trigger |
SL_limit exit is missing trigger. |
Add a positive float. | Enforced |
sl_limit exit requires limit_price |
SL_limit exit is missing limit_price. |
Add a positive float. | Enforced |
gtt exit requires trigger |
GTT exit is missing trigger. |
Add a positive float. | Enforced |
gtt exit requires limit_price |
GTT exit is missing limit_price. |
Add a positive float. | Enforced |
symbol {sent_value!r} does not match the entry that opened order_ref {ref!r}, which is {actual_value!r} — check the order_ref before resending |
Exit/amend/cancel has a symbol that mismatches the entry. |
Verify you are citing the correct order_ref. The message names both values. If the order_ref is wrong, resend to the right one. |
Enforced |
exchange {sent_value!r} does not match the entry that opened order_ref {ref!r}, which is {actual_value!r} — check the order_ref before resending |
Exit/amend/cancel has an exchange that mismatches the entry. |
Verify you are citing the correct order_ref. The message names both values. If the order_ref is wrong, resend to the right one. |
Enforced |
side {sent_value!r} is not valid for a {action} on order_ref {ref!r} — expected {expected!r} |
Exit/amend/cancel has a side that does not match requirements. For an exit, send side: "sell" (closing a position is selling). For amend/cancel, send side: "buy" to match the entry. |
Verify the side field: exits are "sell", amendments and cancellations are "buy". If you are citing the wrong order_ref, that may be the root cause. |
Enforced |
amend carries nothing amendable |
Amend has no amendable fields. | Include at least one of: sl, targets, qty, limit_price, trigger. (expires_at is not amendable; it is every message's own TTL.) |
Enforced |
unknown order_ref |
Amend, exit, or cancel references an order_ref we have no entry for. |
Check the order_ref spelling and ensure the entry signal was accepted first. |
Enforced |
the JSON literal NaN cannot be stored — send a finite number or omit the field |
Your body contains a bare NaN, Infinity or -Infinity literal. These are not valid JSON, though several libraries emit them. |
Verify your calculations do not divide by zero. Python's json.dumps emits them by default — see the callout below. |
Enforced |
the number 1e400 overflows to infinity and cannot be stored — send a value within double precision |
A numeric literal is syntactically valid JSON but too large for a double. | Send a value within double-precision range. | Enforced |
payload contains a NUL character (U+0000), which … |
Any string field contains a NUL, usually arriving as the \u0000 escape. |
Remove it. A NUL in a symbol or id is almost always a bug in string handling upstream. | Enforced |
payload contains an unpaired UTF-16 surrogate (e.g. …) |
A string contains a lone surrogate code point, which is not encodable as UTF-8. | Usually caused by slicing a string mid-emoji or mishandling \uD800-range escapes. |
Enforced |
payload nesting is too deep to parse |
The JSON nests deeper than the parser can handle. | Flatten the structure. Should not arise in a normal instruction. | Enforced |
payload could not be stored (...) |
A catch-all for storage failures not covered above. | Review the error class in the message (e.g., constraint violation, overflow, encoding). Contact support if unclear. | Enforced |
order_ref closed |
Amend or cancel targets a position that is already closed or filled. | Not yet enforced. Today the instruction is accepted with status accepted, but produces per-account skips (a recorded reason per customer account that could not process the instruction). A future release will refuse this with status rejected. |
Not yet enforced |
already filled — send exit |
Cancel targets an order that is already filled. | Send an exit instead to close the position. |
Not yet enforced |
A Word About NaN: The Silent Integration Killer
If you use Python, you must know this:
Python's json.dumps() emits the non-standard NaN literal by default (allow_nan=True). If a stop-loss or limit-price calculation divides by zero, or hits a missing-data edge case, Python produces float('nan') and serialises it as a bare NaN in the body. Every field is present, every type is correct, and the payload looks complete. DxB refuses it with the JSON literal NaN cannot be stored — send a finite number or omit the field.
Two reasons this deserves its own section rather than a table row.
First, it will not fail your code review or your unit tests. NaN propagates silently through arithmetic — no exception, no warning — so the first evidence is a refusal at a customer's market open.
Second, and worth understanding: NaN defeats the obvious guard. A check like if sl > 0 passes a NaN straight through, because every comparison against NaN is false, including NaN > 0, NaN <= 0 and NaN == NaN. So a validation layer that looks correct will not catch it. Use math.isfinite():
if sl is not None and not math.isfinite(sl):
raise ValueError("stop-loss is not a finite number — do not send this signal")
Other languages: JavaScript's JSON.stringify turns NaN into null, which we refuse for a different and clearer reason (a required field is missing). Check with Number.isFinite() before serialising either way.
# Bad: division by zero produces NaN
import json
stop_loss = entry_price - (entry_price / 0) # NaN
json.dumps({"sl": stop_loss}) # {"sl": NaN} — rejected
# Good: guard the edge case
import json
stop_loss = entry_price - (entry_price / distance) if distance > 0 else None
payload = {"sl": stop_loss} # {"sl": null} or omit it — accepted
json.dumps(payload)
If you use another language, you are less likely to hit this, but the principle holds: verify no price or quantity field is NaN or Infinity before you serialize.
Response Handling
When you send a signal, DxB responds with one of the following:
HTTP 413: Request Body Too Large
HTTP/1.1 413 Payload Too Large
Content-Type: application/json
{"detail": "request body too large"}
The request body exceeds 64 KiB. This check happens before authentication — the request is rejected immediately without verifying the signature or any other field. Nothing is recorded on our side, so there is no trace for us to look up if you ask us to investigate. If this happens, trim your payload (e.g., reduce the number of targets, shorten field values) and resend with a new signal_id.
HTTP 401: Authentication Failed
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{"detail": "unauthorized"}
Every authentication failure returns this same body — a missing header, an unrecognised X-DXB-Key, a suspended strategy, a timestamp outside the tolerance window, and a signature mismatch are deliberately indistinguishable. A more specific message would let anyone probing the endpoint discover which API keys are real, so the detail is withheld on purpose rather than by oversight.
That means a 401 needs you to check all four in turn. In practice the overwhelmingly common cause is signing a re-serialized JSON string instead of the raw request body: verify you are hashing the exact bytes that went on the wire, not a freshly stringified object. See Signature Computation. If the bytes are right, check your clock next.
Nothing is recorded on our side for a 401 — an unauthenticated request cannot write to our database at all, so there is no trace for us to look up if you ask us to investigate. Reproduce it against the sandbox instead.
HTTP 200: Acceptance, Duplicate, Staleness, or Rejection
Always 200 OK, with a JSON body:
{
"status": "accepted",
"reason": null
}
The status field tells you the outcome:
| status | reason | What Happened |
|---|---|---|
accepted |
null | Order was successfully ingested and orders are being placed across customer accounts. |
duplicate |
null | We already have a row with this signal_id for this strategy. The prior signal was processed; this one is ignored (the safe outcome of a retry). Nothing changes; no new orders are placed. |
stale |
A message explaining why. | One of three things: signal_ts is older than 900 seconds; expires_at was already past when it reached us; or signal_ts is more than 300 seconds in the future, which means your clock is ahead of ours. Read the reason — it names which. Do not retry unchanged. Age and expiry need a fresh signal; a future timestamp needs your clock resynced. |
rejected |
A specific reason from the refusal table. | The payload is invalid or conflicts with system state (unknown order_ref, short entry, missing required field, etc.). This is not a transient error. Retrying the identical payload will always produce the same rejection. Fix the issue and send a corrected signal with a new signal_id, or contact support. |
Key point: rejected is an answer, not an error. You must fix the problem. Blind retries do not help.
Timestamps
Every instruction carries two timestamps, both required to have explicit timezone offsets:
signal_ts— Your system's clock when the instruction was generated (ISO 8601, e.g.,2026-08-13T09:21:03+05:30). Used to detect stale signals (older than 900 seconds returns statusstale).expires_at— When this instruction stops being valid (ISO 8601, e.g.,2026-08-13T15:30:00+05:30). If already past when we receive the signal, it is refused as expired.
Naive timestamps (without offsets) are refused. We cannot compare your clock to ours without a zone, and guessing one — even as a "sensible default" like UTC — silently shifts the staleness check and expiry window by hours.
Note: These payload timestamps are separate from the X-DXB-Timestamp header used for request authentication. The header uses a tight ±300s window and returns 401 if outside it; the payload signal_ts uses a 900s backward window and returns status stale if older. These are two separate mechanisms (see Response Handling and Before You Go Live item 11).
Why This Matters
DxB lives in UTC, but your strategy may run in a different timezone. When you send signal_ts: 2026-08-13T09:21:03, we have no way to know if that is 09:21 in IST (+05:30) or UTC or New York (−05:00). A wrong guess puts an old signal in the future, or a fresh one in the past. Explicit offsets eliminate that ambiguity.
Use ISO 8601 with offset:
- Correct:
2026-08-13T09:21:03+05:30(IST) - Correct:
2026-08-13T03:51:03Z(UTC, using Z as shorthand for +00:00) - Incorrect:
2026-08-13T09:21:03(no offset; refused)
Corrections and Retractions
To Correct an Entry: Cancel + New Entry
If you sent an entry with a mistake, you must:
- Send a
cancelfor the originalorder_ref. - Send a new
entrywith a newsignal_id(different from the first entry) AND a neworder_ref(different from the cancelled one, since each order_ref may only be used once).
Do NOT reuse a signal_id. Reusing it is a no-op by design (idempotency), so the second message will be silently ignored. Do NOT reuse an order_ref, even for the corrected trade; an order_ref belongs to the entry that first claimed it, so send a distinct one for the new entry.
To Amend a Position: Send amend with New signal_id
Every amendment is a fresh message with its own signal_id. If you need to adjust a stop, send amend with the same order_ref and a new signal_id.
Sizing and the Multiplier Model
Your instructions specify qty — the number of shares to trade. This quantity is sized at your strategy's base_capital, which is the capital amount you designed your strategy around.
Each of your customers has subscribed to your strategy with a multiplier — a factor applied to the base capital. For example:
- Strategy base capital: 1,000,000 (one million)
- Customer A's multiplier: 1.0 → customer capital = 1,000,000
- Customer B's multiplier: 0.5 → customer capital = 500,000
- Customer C's multiplier: 2.0 → customer capital = 2,000,000
When you send an entry with qty: 100, each customer receives:
- Customer A: 100 × 1.0 = 100 shares
- Customer B: 100 × 0.5 = 50 shares
- Customer C: 100 × 2.0 = 200 shares
Quantities are floored to whole shares. A customer whose floored quantity is zero is skipped for that trade with a recorded reason (visible in our audit log).
Sandbox
Before going live, test against our sandbox environment. It runs the identical validation code as production and auto-executes trades against a paper broker (instant fills, no real money).
Sandbox Keys — same URL, the key decides
There is one endpoint. You do not change the URL to switch environments — you change the key.
POST https://api.dxbalgo.com/api/v1/webhook/signals
Your sandbox API key is prefixed test_ (e.g. test_acme-momo-a); your live key is prefixed live_. A test_ key is routed to the sandbox environment and a live_ key to production, on the same URL. The two environments are separate databases with separate credentials — a sandbox signal can never reach a live account.
This means going live is a one-line change: swap the key (and its secret), leave the URL alone. It also means a key pasted into the wrong config fails loudly rather than trading somewhere you did not intend — each environment holds only its own keys, so the other one is simply unknown and refused with a 401.
https://sandbox.dxbalgo.com/api/v1/webhook/signals also still accepts test_ traffic and is not going away. If you have already built against it, nothing breaks. The single URL above is the simpler thing to build against from here.
Testing Market Orders: ref_price
In production, we infer the fill price of a market order from the broker's execution. In sandbox, we have no real broker data, so we cannot do that. Instead, market orders in sandbox require a sandbox-only ref_price field — the price at which you want the simulated fill to occur.
{
"signal_id": "acme-sandbox-001",
"order_ref": "TEST-ORD-1",
"action": "entry",
"side": "buy",
"symbol": "RELIANCE",
"exchange": "NSE",
"order_type": "market",
"qty": 10,
"ref_price": 2900.0,
"signal_ts": "2026-08-13T10:00:00+05:30",
"expires_at": "2026-08-13T15:30:00+05:30"
}
The ref_price field is ignored in production and required in sandbox for market orders only. Limit, sl_limit, and GTT orders in sandbox use their stated limit_price and trigger to simulate fills.
Sandbox Has No Static IP Requirement
In production, live customer accounts require a registered static IP (an NSE/BSE API-trading requirement). Sandbox has no such gate — test freely.
Sandbox Validation Is Identical
The validation code is the same in sandbox and production. Every refusal reason in the refusal table will appear in sandbox exactly as it would in live. Use sandbox to verify your integration is airtight before flipping the switch.
Before You Go Live: Integration Checklist
- Signature: Test
HMAC-SHA256(secret, timestamp + "." + raw_body)with the exact request bytes. Verify that re-serializing JSON breaks the signature and then fix it. - Timestamp Format: Use ISO 8601 with explicit timezone offsets. Verify that naive timestamps (no offset) are rejected.
- Idempotency: Send the same
signal_idtwice (simulate a retry) and confirm you get{status: "duplicate"}and no new orders. - All Four Verbs: Test at least one entry, one exit, one amend, and one cancel.
- Order Type Matrix: Test market, limit, sl_limit, and GTT for both entry and exit. Verify that forbidden price fields are rejected on each order type.
5a. Exit Order Types: Send exits as market, limit, sl_limit, and GTT. Confirm that order_type is required on every exit and a missing one is refused with "exit requires order_type".
- Short Entry: Confirm that
side: "sell"on an entry is refused with "short entry not supported (CNC only)". - SL and Targets (Reference-Only): Enter a position with both an
sland target prices; amend the SL; send a partial exit. Confirm thatsland targets are both recorded and visible (in audit logs or a test dashboard), but that the position does NOT automatically close when price would touch either level. Verify that YOU must send an explicitexitsignal to close it. This is the critical gate: if your test confirms that stops and targets are NOT executed automatically, your integration is ready.
7a. Identity Field Cross-Check: Send an exit with a symbol and exchange that match the entry (correct); confirm it is accepted. Then send an exit for the same order_ref with a mismatched symbol; confirm it is refused with a reason naming both values. Repeat for exchange and side.
7b. Expires_at is Not Amendable: Send an amend with only expires_at and no other amendable fields; confirm it is refused with "amend carries nothing amendable (one of: sl, targets, qty, limit_price, trigger)".
- Refusals: Trigger at least five refusals from the table (e.g., missing qty, naive timestamp, limit_price on a market order) and confirm you receive the exact reason text.
- Response Codes: Confirm that bad signatures receive a 401, and all other outcomes receive a 200 with a
statusfield. - Sandbox Completeness: Test everything above in sandbox before asking to be moved to live.
- Two timestamp mechanisms (both separate):
- Header skew (authentication): Send a request with an
X-DXB-Timestampfrom 6 minutes ago. Confirm it receives a 401 Unauthorized with{detail: "unauthorized"}and nothing is recorded. This is an auth failure, not a business rejection. - Payload staleness: Send a signal with a
signal_tsfrom 20 minutes ago. Confirm it receives a 200 OK with{status: "stale", reason: "..."}and a row is recorded. This is a business-logic staleness check. These are two separate knobs; do not confuse them in your logs.
- API Key Prefix: Confirm that
live_keys work on the live endpoint only, andtest_keys work on the sandbox endpoint only. - Multiplier Math: With a test customer at 0.5× multiplier, send an entry with
qty: 10and confirm the account receives 5 shares (or 0 if a customer's floored qty is 0, with a recorded reason). - Clock Skew Recovery: If you receive a stale rejection, verify that correcting your system clock and resending (with a new
signal_id) succeeds.
Once all 14 are passing, contact ops to move your API key from sandbox to live.