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 createdaccount_updated
: An existing account has been updatedchallenge_created
: A new PROP challenge has been createdchallenge_updated
: An existing PROP challenge has been updatedopen_position
: A new position has been opened on the marketclose_position
: An existing position has been closed on the marketopen_order
: A new order has been placed (BUY_LIMIT, SELL_LIMIT, etc...)cancel_order
: An existing order has been cancelledbalance_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 accountsdemo
: Only send events for demo accountsboth
: 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.