SDKs
JavaScript/Typescript
Admin Methods
Projects (Brands)
Webhooks

Webhooks

A webhook subscription can be made on a project-per-project basis to receive notifications about the project or the users of the project.

Each project may define multiple URL on which the notifications will be sent. The URL must be a valid URL and must be reachable from the internet. The URL must be unique for each project. Https is not required but recommended although self-signed certificates are also supported.

When defining a webhook, the following events can be subscribed to:

  • account_created: A new account has been created
  • account_updated: An existing account has been updated
  • challenge_created: A new PROP challenge has been created
  • challenge_updated: An existing PROP challenge has been updated
  • open_position: A new position has been opened on the market
  • close_position: An existing position has been closed on the market
  • open_order: A new order has been placed (BUY_LIMIT, SELL_LIMIT, etc...)
  • cancel_order: An existing order has been cancelled
  • balance_operation: A balance operation has been performed (deposit, withdrawal, commission, etc...)

More events will be added in the future and/or upon request.

Along with the webhook URL you must also define a secret. The secret is a string that will be sent along with the webhook payload in the x-webhook-secret header. This can be used to verify that the webhook is coming from the expected source.

Please Note: The secret will be hashed with SHA1 with a hex digest and NOT sent in plain text.

Request Failures & Retries

When an event occurs, the webhook will be sent as a POST request to the defined URL. If the request fails, the webhook will be retried with an exponential backoff between each retry (capped at five (5) minutes) until the endpoint responds with a 2xx status code.

If the endpoint responds with a 404 the notification will be discarded and no further retries will be made.

Webhook Payloads

The subscribed events will be sent as a POST request to the defined URL in the webhook. The events will be sent as an array under a field called payload in the post.

Additional data will be sent along with the payload which provide extra information about the callback being received, such as the number of events in the payload and the number of delivery attempts made. This data will be set in the root of the request sent.

interface WebhooksPayload {
  brandId: number;
  brandUserId?: number;
  serverId: number;
  accountId?: number;
  exchangeAccountId?: string;
  eventType: WebhookEventType;
  platform: Platform;
  payload: Record<string, any>;
}
 
enum Platform {
  MT4 = 'MT4',
  MT5 = 'MT5',
  CT = 'CT',
  TL = 'TL',
}
 
enum WebhookEventType {
  ACCOUNT_CREATED = 'account_created',
  ACCOUNT_UPDATED = 'account_updated',
  CHALLENGE_CREATED = 'challenge_created',
  CHALLENGE_UPDATED = 'challenge_updated',
  OPEN_POSITION = 'open_position',
  CLOSE_POSITION = 'close_position',
  OPEN_ORDER = 'open_order',
  CANCEL_ORDER = 'cancel_order',
  BALANCE_OPERATION = 'balance_operation',
}

Each event type will have a different payload. The payload will be sent as a JSON object in the payload field of the payload. The payloads will contain the following interfaces:

interface AccountCreatedEvent {
  id: number;
  /** The type of the trading platform the account exists on */
  platform: Platform;
  /** Defines how the account can be used */
  type: AccountType;
  /** Defines if the account is active or not */
  status: AccountStatus;
  /** Defines whether the account is a demo account or a real account */
  monetization: Monetization;
  /** The leverage the account has on the platform */
  leverage: number;
  /** ISO 4217 - The currency of the account */
  currency: Currency;
  /** The first name of the account holder */
  firstName?: string;
  /** The last name of the account holder */
  lastName?: string;
  /** The email of the account holder */
  email: string;
  /** Information about the account's groups */
  groups: {
    /** The id of the user group the account exists in */
    userGroupId: number;
  };
  /** Additional metadata which can be stored with the account */
  meta?: Record<string, any>;
  /** The date the entity was created (UTC millisecond timestamp) */
  createdAt: number;
}
 
interface AccountUpdatedEvent extends AccountCreatedEvent {
  /** The date the entity was last updated (UTC millisecond timestamp) */
  updatedAt: number;
}
 
interface PositionOpenEvent {
  id: number;
  platformOperationId: string;
  platformTradeId: string;
  symbol: string;
  openPrice: number;
  takeProfit?: number;
  stopLoss?: number;
  lots: number;
  contractSize?: number;
  type: PositionType;
  ip?: string;
  country?: string;
  balance: number;
  openedAt: Date;
}
 
interface PositionClosedEvent {
  id: number;
  platformOperationId: string;
  platformTradeId: string;
  symbol: string;
  openPrice: number;
  closePrice: number;
  takeProfit?: number;
  stopLoss?: number;
  profit: number;
  profitUSD?: number;
  lots: number;
  contractSize?: number;
  type: PositionType;
  ip?: string;
  country?: string;
  balance: number;
  openedAt: Date;
  closedAt: Date;
}
 
interface OrderOpenEvent {
  id: number;
  platformOperationId: string;
  symbol: string;
  openPrice: number;
  takeProfit?: number;
  stopLoss?: number;
  lots: number;
  contractSize?: number;
  type: OrderType;
  ip?: string;
  country?: string;
  placedAt: Date;
}
 
interface OrderCancelledEvent {
  id: number;
  platformOperationId: string;
  symbol: string;
  openPrice: number;
  takeProfit?: number;
  stopLoss?: number;
  lots: number;
  contractSize?: number;
  type: OrderType;
  ip?: string;
  country?: string;
  placedAt: Date;
  cancelledAt: Date;
}
 
interface BalanceTransactionPayload {
  id: number;
  platformOperationId: string;
  referenceId: string;
  amount: number;
  currency: string;
  operation: BalanceOperation;
  ip?: string;
  comment?: string;
  balance: number;
  createdAt: Date;
}
 
// Enums
//////////////////////////////
enum PositionType {
  BUY = 'BUY',
  SELL = 'SELL',
}
 
enum OrderType {
  BUY_LIMIT = 'BUY_LIMIT',
  SELL_LIMIT = 'SELL_LIMIT',
  BUY_STOP = 'BUY_STOP',
  SELL_STOP = 'SELL_STOP',
  BUY_STOP_LIMIT = 'BUY_STOP_LIMIT',
  SELL_STOP_LIMIT = 'SELL_STOP_LIMIT',
}
 
enum BalanceOperation {
  ADD = 'add',
  SUB = 'sub',
  ADJUSTMENT = 'adjustment',
  COMMISSION = 'commission',
  SWAP = 'swap',
  CREDIT = 'credit',
  DEBIT = 'debit',
}
//////////////////////////////
ℹ️

Challenge webhooks will contain the same payload as is returned by the PROP api. For more information on the PROP API, please refer to the PROP API swagger documentation (opens in a new tab).

Managing Webhooks

In order to create a webhook subscription, you may use the createWebhook method under the admin.brand namespace in the SDK.

import { Webhook } from '@tradrapi/trading-sdk';
 
const webhook: Webhook = await tradrapi.admin.brand.createWebhook({
  url: 'https://example.com/webhook',
  key: 'my-secret-key',
  eventTypes: ['open_position','close_position'],
  eventMonetization: 'real',
  maxSize: 100,
  maxWaitTime: 60,
});

Separate webhook subscriptions can be made for different events. This allows you to have different endpoints for different events.

For example:

import { Webhook } from '@tradrapi/trading-sdk';
 
const positionsWebhook: Webhook = await tradrapi.admin.brand.createWebhook({
  url: 'https://example.com/positions-endpoint',
  key: 'my-secret-key',
  eventTypes: ['open_position','close_position'],
});
 
const balanceOperations: Webhook = await tradrapi.admin.brand.createWebhook({
  url: 'https://example.com/balance-ops-endpoint',
  key: 'my-secret-key',
  eventTypes: ['balance_operation'],
});

As always, the data is scoped to the brand which is creating the webhook. The create request will be automatically add the appropriate brandId if using the SDK alongside a valid API key.

When creating a webhook, the eventMonetization parameter can be used to filter the events sent based on the monetization type of the account they belong to. This parameter can be set to one of the following values:

  • real: Only send events for real accounts
  • demo: Only send events for demo accounts
  • both: Send events for both real and demo accounts

The createWebhook method accepts a CreateWebhookDto which has the following parameters:

interface CreateWebhookDto {
  /** The webhook endpoint which will receive events */
  url: string;
  /**
   * The key which will be sent in the x-webhook-secret header
   * @minLength 20
   * @maxLength 140
   */
  key: string;
  /**
   * Indicates if this webhook is disabled
   * @default true
   */
  isEnabled?: boolean;
  /** The list of events which will be sent to the webhook URL */
  eventTypes: (
    | 'open_position'
    | 'close_position'
    | 'open_order'
    | 'cancel_order'
    | 'balance_operation'
  )[];
  /**
   * Filters events sent based on the monetization type of the account they belong to
   * @default "both"
   */
  eventMonetization?: 'real' | 'demo' | 'both';
  /**
   * The maximum number of events that should be in a batch. A batch is guaranteed to never have more
   * than this number of events, however it can have fewer events.
   * @min 1
   * @max 500
   * @default 50
   */
  maxSize?: number;
  /**
   * The maximum amount of time to wait in seconds before the batch is sent regardless of its size.
   * @min 1
   * @max 300
   * @default 120
   */
  maxWaitTime?: number;
}

An existing webhook can be updated using the updateWebhook method under the admin.brand namespace in the SDK.

import { Webhook } from '@tradrapi/trading-sdk';
 
const  webhookId = 1;
const webhook: Webhook = await tradrapi.admin.brand.updateWebhook(webhookId, {
  url: 'https://example.com/new-webhook-endpoint',
  maxSize: 200,
});

The updateWebhook method accepts a UpdateWebhookDto which has the following optional parameters:

interface UpdateWebhookDto {
  /** The webhook endpoint which will receive events */
  url?: string;
  /**
   * The key which will be sent in the x-webhook-secret header
   * @minLength 20
   * @maxLength 140
   */
  key?: string;
  /**
   * Indicates if this webhook is disabled
   * @default true
   */
  isEnabled?: boolean;
  /** The list of events which will be sent to the webhook URL */
  eventTypes?: (
    | 'open_position'
    | 'close_position'
    | 'open_order'
    | 'cancel_order'
    | 'balance_operation'
  )[];
  /**
   * Filters events sent based on the monetization type of the account they belong to
   * @default "both"
   */
  eventMonetization?: 'real' | 'demo' | 'both';
  /**
   * The maximum number of events that should be in a batch. A batch is guaranteed to never have more
   * than this number of events, however it can have fewer events.
   * @min 1
   * @max 500
   * @default 50
   */
  maxSize?: number;
  /**
   * The maximum amount of time to wait in seconds before the batch is sent regardless of its size.
   * @min 1
   * @max 300
   * @default 120
   */
  maxWaitTime?: number;
}

An existing webhook can be deleted using the deleteWebhook method under the admin.brand namespace in the SDK.

import { Webhook } from '@tradrapi/trading-sdk';
 
const  webhookId = 1;
const result: boolean = await tradrapi.admin.brand.deleteWebhook(webhookId);

It is also possible to list the webhook configuration for your project using the listWebhooks method under the admin.brand namespace in the SDK.

import { Webhook } from '@tradrapi/trading-sdk';
 
const webhook: Webhook = await tradrapi.admin.brand.listWebhooks();

The response for the listWebhooks method will consist of a paginated list of all webhooks which have been configured. In all cases were a Webhook object is returned, the object will have the following structure:

interface Webhook {
  /** The webhook endpoint which will receive events */
  url: string;
  /** Indicates if this webhook is disabled */
  isEnabled?: boolean;
  /** The list of events which will be sent to the webhook URL */
  eventTypes: (
    | 'account_created'
    | 'account_updated'
    | 'challenge_created'
    | 'challenge_updated'
    | 'open_position'
    | 'close_position'
    | 'open_order'
    | 'cancel_order'
    | 'balance_operation'
  )[];
  /** Filters events sent based on the monetization type of the account they belong to */
  eventMonetization?: 'real' | 'demo' | 'both';
  /** The maximum number of events that should be in a batch. A batch is guaranteed to never have more than this number of events, however it can have fewer events. */
  maxSize?: number;
  /** The maximum amount of time to wait in seconds before the batch is sent regardless of its size. */
  maxWaitTime?: number;
  /** The brand ID the webhook configuration belongs to */
  brandId: number;
  /** The TradrAPI ID of the entity */
  id: number;
  /**
   * The date the entity was created
   * @format date-time
   */
  createdAt: string;
  /**
   * The date the entity was last updated
   * @format date-time
   */
  updatedAt: string;
}

Webhook audits

The messages which a webhook sends to any defined URL can be audited to ensure that the messages are being sent and to identify any messages which were sent but failed delivery. Although TradrAPI will retry sending messages which fail to be delivered, it is important to understand that the number of automatic re-attempts are not infinite.

Furthermore, the audit logs can be used to identify any issues with the webhook configuration, such as a URL which is incorrectly configured or a secret which is not being verified correctly.

To list the audit logs for your webhook, you may use the listWebhookMessages method under the admin.brand namespace in the SDK.

import { PaginatedResponse, WebhookMessageDto } from '@tradrapi/trading-sdk';
 
const webhookId = 1;
const filter = {
  limit: 10,
  offset: 0,
  from: '2024-01-01T00:00:00Z',
  to: '2024-01-31T23:59:59Z',
}
 
const result: PaginatedResponse<WebhookMessageDto> = await tradrapi.admin.brand.listWebhookMessages(webhookId, filter);
⚠️

Audit logs are only available for the last 120 days. If you require logs for a longer period, you will need to store the logs on your end.

Similarly, to list a single audit log for your webhook, you may use the getWebhookMessage method under the same namespace, this is provided you know the messageId beforehand.

Reattempting Deliveries

Using the SDK you can request a re-delivery of a message which has failed and is no longer being retried. This can be done using the rescheduleWebhookMessage method under the admin.brand namespace in the SDK.

const webhookId = 1;
const messageId = 1432;
 
const result: boolean = await tradrapi.admin.brand.rescheduleWebhookMessage(webhookId, messageId);

A message can only be re-scheduled if it is no longer being retried by the system and exists in a permanent failed state.

⚠️

Whilst re-scheduling a message is instant, it make take a few seconds to a minute for the delivery to be attempted.