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:

Signature Computation

The signature verifies that you, not an attacker, composed this request. Compute it as follows:

  1. Get the raw request body as bytes (the literal string that went on the wire, before any JSON parsing or reformatting).
  2. Get the value of the X-DXB-Timestamp header.
  3. Concatenate: timestamp + "." + raw_body (e.g., "2026-08-13T10:45:30+05:30." + the_raw_bytes).
  4. HMAC-SHA256 using your API secret as the key.
  5. Encode the result as hexadecimal.
  6. Place it in the X-DXB-Signature header.

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.

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:

Optional fields:

Forbidden fields:

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:

Optional fields:

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.

Forbidden fields:

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:

Amendable fields:

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:

Optional fields:

Forbidden:

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:

Optional fields:

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:

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:

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:

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:

Corrections and Retractions

To Correct an Entry: Cancel + New Entry

If you sent an entry with a mistake, you must:

  1. Send a cancel for the original order_ref.
  2. Send a new entry with a new signal_id (different from the first entry) AND a new order_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:

When you send an entry with qty: 100, each customer receives:

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

  1. 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.
  2. Timestamp Format: Use ISO 8601 with explicit timezone offsets. Verify that naive timestamps (no offset) are rejected.
  3. Idempotency: Send the same signal_id twice (simulate a retry) and confirm you get {status: "duplicate"} and no new orders.
  4. All Four Verbs: Test at least one entry, one exit, one amend, and one cancel.
  5. 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".

  1. Short Entry: Confirm that side: "sell" on an entry is refused with "short entry not supported (CNC only)".
  2. SL and Targets (Reference-Only): Enter a position with both an sl and target prices; amend the SL; send a partial exit. Confirm that sl and 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 explicit exit signal 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)".

  1. 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.
  2. Response Codes: Confirm that bad signatures receive a 401, and all other outcomes receive a 200 with a status field.
  3. Sandbox Completeness: Test everything above in sandbox before asking to be moved to live.
  4. Two timestamp mechanisms (both separate):
  1. API Key Prefix: Confirm that live_ keys work on the live endpoint only, and test_ keys work on the sandbox endpoint only.
  2. Multiplier Math: With a test customer at 0.5× multiplier, send an entry with qty: 10 and confirm the account receives 5 shares (or 0 if a customer's floored qty is 0, with a recorded reason).
  3. 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.