Skip to main content
This page documents every event type that Buildmarkets can deliver to your webhook endpoint, organized by domain. Each entry includes the event name, when it fires, and the full JSON payload structure. For information on registering webhooks and managing subscriptions, see Webhooks Overview. For signature verification and delivery guarantees, see Webhook Security & Delivery.

Payload envelope

Every webhook event shares the same outer envelope structure, regardless of event type:
{
  "id": "evt_a1b2c3d4e5f6",
  "event_type": "order.filled",
  "created_at": "2026-03-24T14:32:00Z",
  "data": {
    // Event-specific payload
  }
}
FieldTypeDescription
idstringUnique identifier for this event delivery
event_typestringThe event type name (see tables below)
created_atstring (ISO 8601)Timestamp when the event was generated
dataobjectEvent-specific payload — fields vary by event_type

All event types

DomainEventWhen it fires
Accountaccount.kyc_approvedKYC review passed; account is now ACTIVE
Accountaccount.kyc_rejectedKYC review failed; account is now ACTION_REQUIRED
Accountaccount.closedAccount has been successfully closed
Fundingfunding.ach_relationship_createdAn ACH relationship has been created and approved
Fundingfunding.ach_relationship_removedAn ACH relationship has been cancelled
Fundingfunding.deposit_completedAn ACH deposit has fully settled
Fundingfunding.deposit_returnedAn ACH deposit was returned by the bank
Fundingfunding.withdrawal_completedAn ACH withdrawal has fully settled
Fundingfunding.withdrawal_returnedAn ACH withdrawal was returned by the bank
Tradingorder.submittedAn order was accepted and submitted to the market
Tradingorder.filledAn order was fully executed
Tradingorder.rejectedAn order was rejected before or during execution
Tradingorder.cancelledAn order was cancelled (user-initiated or system)

Account events

account.kyc_approved

Fires when a new account completes KYC review and transitions to ACTIVE status. This is typically the signal your application should act on to enable trading and funding features for a user.
{
  "id": "evt_a1b2c3d4e5f6",
  "event_type": "account.kyc_approved",
  "created_at": "2026-03-24T09:15:00Z",
  "data": {
    "account_id": "b3d9f1a2-4c7e-4f0b-9e2d-1a2b3c4d5e6f",
    "status": "ACTIVE",
    "approved_at": "2026-03-24T09:15:00Z"
  }
}

account.kyc_rejected

Fires when KYC review fails. The account transitions to ACTION_REQUIRED. Your application should prompt the user to provide additional documentation or contact support.
{
  "id": "evt_b2c3d4e5f6a7",
  "event_type": "account.kyc_rejected",
  "created_at": "2026-03-24T09:18:00Z",
  "data": {
    "account_id": "b3d9f1a2-4c7e-4f0b-9e2d-1a2b3c4d5e6f",
    "status": "ACTION_REQUIRED",
    "rejection_reason": "IDENTITY_VERIFICATION_FAILED",
    "rejected_at": "2026-03-24T09:18:00Z"
  }
}

account.closed

Fires when an account closure completes. The account status transitions to CLOSED.
{
  "id": "evt_c3d4e5f6a7b8",
  "event_type": "account.closed",
  "created_at": "2026-03-24T16:00:00Z",
  "data": {
    "account_id": "b3d9f1a2-4c7e-4f0b-9e2d-1a2b3c4d5e6f",
    "status": "CLOSED",
    "closed_at": "2026-03-24T16:00:00Z"
  }
}

Funding events

funding.ach_relationship_created

Fires when a new ACH relationship is successfully created and passes verification, entering APPROVED status.
{
  "id": "evt_d4e5f6a7b8c9",
  "event_type": "funding.ach_relationship_created",
  "created_at": "2026-03-15T10:30:01Z",
  "data": {
    "account_id": "b3d9f1a2-4c7e-4f0b-9e2d-1a2b3c4d5e6f",
    "ach_relationship_id": "e7f2a1b3-9c4d-4e0f-8a1b-2c3d4e5f6a7b",
    "status": "APPROVED",
    "bank_account_number": "****9012",
    "bank_routing_number": "021000021",
    "bank_account_type": "CHECKING"
  }
}

funding.ach_relationship_removed

Fires when an ACH relationship is cancelled via DELETE /v1/accounts/{accountId}/ach-relationships/{achId}.
{
  "id": "evt_e5f6a7b8c9d0",
  "event_type": "funding.ach_relationship_removed",
  "created_at": "2026-03-20T11:00:00Z",
  "data": {
    "account_id": "b3d9f1a2-4c7e-4f0b-9e2d-1a2b3c4d5e6f",
    "ach_relationship_id": "e7f2a1b3-9c4d-4e0f-8a1b-2c3d4e5f6a7b",
    "status": "CANCELLED"
  }
}

funding.deposit_completed

Fires when an ACH deposit fully settles and funds are available in the account.
{
  "id": "evt_f6a7b8c9d0e1",
  "event_type": "funding.deposit_completed",
  "created_at": "2026-03-17T08:30:00Z",
  "data": {
    "account_id": "b3d9f1a2-4c7e-4f0b-9e2d-1a2b3c4d5e6f",
    "activity_id": "f1a2b3c4-d5e6-7f8a-9b0c-d1e2f3a4b5c6",
    "ach_relationship_id": "e7f2a1b3-9c4d-4e0f-8a1b-2c3d4e5f6a7b",
    "amount": "2500.00",
    "status": "SETTLED",
    "settled_at": "2026-03-17T08:30:00Z"
  }
}

funding.deposit_returned

Fires when an ACH deposit is returned by the bank. The return_code identifies the reason.
{
  "id": "evt_a7b8c9d0e1f2",
  "event_type": "funding.deposit_returned",
  "created_at": "2026-03-12T16:45:00Z",
  "data": {
    "account_id": "b3d9f1a2-4c7e-4f0b-9e2d-1a2b3c4d5e6f",
    "activity_id": "b2c3d4e5-f6a7-8b9c-0d1e-2f3a4b5c6d7e",
    "ach_relationship_id": "e7f2a1b3-9c4d-4e0f-8a1b-2c3d4e5f6a7b",
    "amount": "500.00",
    "status": "RETURNED",
    "return_code": "R01",
    "return_reason": "Insufficient Funds"
  }
}

funding.withdrawal_completed

Fires when an ACH withdrawal fully settles at the destination bank.
{
  "id": "evt_b8c9d0e1f2a3",
  "event_type": "funding.withdrawal_completed",
  "created_at": "2026-03-18T09:00:00Z",
  "data": {
    "account_id": "b3d9f1a2-4c7e-4f0b-9e2d-1a2b3c4d5e6f",
    "activity_id": "c4d5e6f7-a8b9-0c1d-2e3f-4a5b6c7d8e9f",
    "ach_relationship_id": "e7f2a1b3-9c4d-4e0f-8a1b-2c3d4e5f6a7b",
    "amount": "500.00",
    "status": "SETTLED",
    "settled_at": "2026-03-18T09:00:00Z"
  }
}

funding.withdrawal_returned

Fires when an ACH withdrawal is returned by the bank.
{
  "id": "evt_c9d0e1f2a3b4",
  "event_type": "funding.withdrawal_returned",
  "created_at": "2026-03-19T14:00:00Z",
  "data": {
    "account_id": "b3d9f1a2-4c7e-4f0b-9e2d-1a2b3c4d5e6f",
    "activity_id": "d5e6f7a8-b9c0-d1e2-f3a4-b5c6d7e8f9a0",
    "ach_relationship_id": "e7f2a1b3-9c4d-4e0f-8a1b-2c3d4e5f6a7b",
    "amount": "250.00",
    "status": "RETURNED",
    "return_code": "R02",
    "return_reason": "Account Closed"
  }
}

Trading events

order.submitted

Fires when an order has been accepted by Buildmarkets and submitted to the market. The order is now in pending_new or new status.
{
  "id": "evt_d0e1f2a3b4c5",
  "event_type": "order.submitted",
  "created_at": "2026-03-24T14:00:00Z",
  "data": {
    "account_id": "b3d9f1a2-4c7e-4f0b-9e2d-1a2b3c4d5e6f",
    "order_id": "ord_1a2b3c4d-5e6f-7a8b-9c0d-e1f2a3b4c5d6",
    "symbol": "AAPL",
    "side": "buy",
    "order_type": "market",
    "quantity": "10",
    "status": "new",
    "submitted_at": "2026-03-24T14:00:00Z"
  }
}

order.filled

Fires when an order is fully executed. Includes the average fill price and total filled quantity.
{
  "id": "evt_e1f2a3b4c5d6",
  "event_type": "order.filled",
  "created_at": "2026-03-24T14:00:03Z",
  "data": {
    "account_id": "b3d9f1a2-4c7e-4f0b-9e2d-1a2b3c4d5e6f",
    "order_id": "ord_1a2b3c4d-5e6f-7a8b-9c0d-e1f2a3b4c5d6",
    "symbol": "AAPL",
    "side": "buy",
    "order_type": "market",
    "quantity": "10",
    "filled_quantity": "10",
    "average_fill_price": "213.49",
    "status": "filled",
    "filled_at": "2026-03-24T14:00:03Z"
  }
}

order.rejected

Fires when an order is rejected — either by Buildmarkets validation or by the downstream execution venue. The rejection_reason explains why.
{
  "id": "evt_f2a3b4c5d6e7",
  "event_type": "order.rejected",
  "created_at": "2026-03-24T14:00:01Z",
  "data": {
    "account_id": "b3d9f1a2-4c7e-4f0b-9e2d-1a2b3c4d5e6f",
    "order_id": "ord_2b3c4d5e-6f7a-8b9c-0d1e-2f3a4b5c6d7e",
    "symbol": "AAPL",
    "side": "buy",
    "order_type": "market",
    "quantity": "10000",
    "status": "rejected",
    "rejection_reason": "INSUFFICIENT_BUYING_POWER",
    "rejected_at": "2026-03-24T14:00:01Z"
  }
}

order.cancelled

Fires when an order is cancelled — either by the user (DELETE /v1/accounts/{id}/orders/{orderId}) or by the system (e.g., day order not filled by market close).
{
  "id": "evt_a3b4c5d6e7f8",
  "event_type": "order.cancelled",
  "created_at": "2026-03-24T16:00:01Z",
  "data": {
    "account_id": "b3d9f1a2-4c7e-4f0b-9e2d-1a2b3c4d5e6f",
    "order_id": "ord_3c4d5e6f-7a8b-9c0d-e1f2-a3b4c5d6e7f8",
    "symbol": "TSLA",
    "side": "buy",
    "order_type": "limit",
    "quantity": "5",
    "filled_quantity": "0",
    "status": "cancelled",
    "cancel_reason": "DAY_ORDER_EXPIRED",
    "cancelled_at": "2026-03-24T16:00:01Z"
  }
}

Handling duplicate events

In rare cases (network issues, retries), your endpoint may receive the same event more than once. Use the id field on each event payload as an idempotency key — store processed event IDs and skip processing if the ID has already been handled.
const processedEventIds = new Set(); // or use a persistent store

app.post('/webhooks/buildmarkets', (req, res) => {
  const event = req.body;
  
  if (processedEventIds.has(event.id)) {
    return res.status(200).json({ received: true }); // Already processed — skip
  }
  
  processedEventIds.add(event.id);
  // ... process event
  res.status(200).json({ received: true });
});

Next steps

Updated 3 months ago

Webhook Security & Delivery

Every webhook payload Buildmarkets sends is cryptographically signed. Verifying this signature before processing an event is critical — it ensures the payload genuinely came from Buildmarkets and has not been tampered with. This page explains how to verify signatures, what delivery guarantees Buildmarkets provides, how to inspect delivery history, and best practices for building a reliable webhook handler.

Signature verification

When you register a webhook, Buildmarkets returns a secret that begins with whsec_. Buildmarkets uses this secret to compute an HMAC-SHA256 signature of the raw request body and attaches it to every delivery via the X-BMKT-Signature header. Your endpoint must verify this signature before acting on any payload.

Verification steps

  1. Read the raw request body as bytes (do not parse JSON first)
  2. Read the X-BMKT-Signature header
  3. Compute HMAC-SHA256(raw_body, secret)
  4. Compare your computed signature to the value in the header
  5. If they match — process the event. If not — return 400 and ignore the payload
Important: Always use a constant-time comparison function when comparing signatures to prevent timing attacks. Do not use simple string equality (===).

Example — Node.js (Express)

const crypto = require('crypto');
const express = require('express');
const app = express();

// Use raw body parser to preserve the exact bytes for HMAC
app.use('/webhooks/buildmarkets', express.raw({ type: 'application/json' }));

const WEBHOOK_SECRET = process.env.BUILDMARKETS_WEBHOOK_SECRET; // your whsec_... value

app.post('/webhooks/buildmarkets', (req, res) => {
  const signature = req.headers['x-buildmarkets-signature'];
  if (!signature) {
    return res.status(400).send('Missing signature');
  }

  // Compute expected signature
  const expectedSig = crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(req.body) // req.body is raw bytes when using express.raw()
    .digest('hex');

  // Constant-time comparison
  const sigBuffer = Buffer.from(signature);
  const expectedBuffer = Buffer.from(expectedSig);

  if (
    sigBuffer.length !== expectedBuffer.length ||
    !crypto.timingSafeEqual(sigBuffer, expectedBuffer)
  ) {
    return res.status(400).send('Invalid signature');
  }

  // Signature verified — parse and process
  const event = JSON.parse(req.body);
  console.log('Received event:', event.event_type);

  // ... handle event

  res.status(200).json({ received: true });
});

Example — Python (Flask)

import hmac
import hashlib
import os
from flask import Flask, request, abort

app = Flask(__name__)
WEBHOOK_SECRET = os.environ['BUILDMARKETS_WEBHOOK_SECRET'].encode()

@app.route('/webhooks/buildmarkets', methods=['POST'])
def handle_webhook():
    signature = request.headers.get('X-BMKT-Signature')
    if not signature:
        abort(400, 'Missing signature')

    # Compute expected signature over raw bytes
    expected_sig = hmac.new(
        WEBHOOK_SECRET,
        request.data,  # Raw bytes, before JSON parsing
        hashlib.sha256
    ).hexdigest()

    # Constant-time comparison
    if not hmac.compare_digest(signature, expected_sig):
        abort(400, 'Invalid signature')

    # Parse and process
    event = request.get_json()
    print(f"Received event: {event['event_type']}")

    # ... handle event

    return {'received': True}, 200

Delivery mechanics

Endpoint requirements

Your webhook URL must:
  • Be publicly accessible via HTTPS (TLS 1.2 or higher)
  • Return a 2xx HTTP status code within 10 seconds
  • Accept POST requests with Content-Type: application/json
Buildmarkets treats any non-2xx response (including 3xx redirects) as a delivery failure and will retry.

Retry schedule

If your endpoint does not return a 2xx within 10 seconds, Buildmarkets retries delivery with exponential backoff:
AttemptDelay after previous attempt
1st retry5 minutes
2nd retry30 minutes
3rd retry2 hours
4th retry5 hours
5th retry10 hours
After 5 failed retries, the delivery is marked as permanently failed. Buildmarkets does not attempt further delivery for that event. Check the delivery history endpoint to identify and investigate failed events.
Your endpoint should return 2xx quickly, then process asynchronously. If your handler takes more than a few seconds, return 200 immediately and offload processing to a background queue. This prevents timeout-induced retries for events you have already received.

At-least-once delivery

Buildmarkets guarantees at-least-once delivery — every event will be delivered at least once, but rare network conditions can result in duplicate deliveries. Use the id field from the payload envelope as an idempotency key to detect and safely ignore duplicates.

Delivery history

The deliveries endpoint provides a log of every delivery attempt made for a specific webhook — including the HTTP status code your server returned, the response time, and whether the attempt succeeded.

GET /v1/webhooks/{webhookId}/deliveries

Path parameters
ParameterTypeRequiredDescription
webhookIdstring (UUID)The webhook to retrieve delivery history for
Response fields
Each delivery attempt object contains:
FieldTypeDescription
idstringUnique delivery attempt ID
event_idstringID of the event this delivery corresponds to
event_typestringThe event type that was delivered
statusstringsucceeded or failed
http_statusinteger | nullHTTP status code returned by your endpoint
response_time_msinteger | nullTime in milliseconds for your endpoint to respond
attempt_numberintegerWhich delivery attempt this was (1 = first, 2 = first retry, etc.)
attempted_atstring (ISO 8601)Timestamp of the delivery attempt
next_retry_atstring (ISO 8601) | nullWhen the next retry will be attempted; null if succeeded or max retries reached
Example request
curl https://api.tappengine.com/v1/webhooks/wh_a1b2c3d4-e5f6-7a8b-9c0d-e1f2a3b4c5d6/deliveries \
  -H "Authorization: Bearer $BUILDMARKETS_API_KEY"
Example response
[
  {
    "id": "del_a1b2c3d4",
    "event_id": "evt_e1f2a3b4c5d6",
    "event_type": "order.filled",
    "status": "succeeded",
    "http_status": 200,
    "response_time_ms": 142,
    "attempt_number": 1,
    "attempted_at": "2026-03-24T14:00:04Z",
    "next_retry_at": null
  },
  {
    "id": "del_b2c3d4e5",
    "event_id": "evt_d0e1f2a3b4c5",
    "event_type": "order.submitted",
    "status": "failed",
    "http_status": 503,
    "response_time_ms": 9823,
    "attempt_number": 1,
    "attempted_at": "2026-03-24T14:00:01Z",
    "next_retry_at": "2026-03-24T14:05:01Z"
  },
  {
    "id": "del_c3d4e5f6",
    "event_id": "evt_d0e1f2a3b4c5",
    "event_type": "order.submitted",
    "status": "succeeded",
    "http_status": 200,
    "response_time_ms": 88,
    "attempt_number": 2,
    "attempted_at": "2026-03-24T14:05:01Z",
    "next_retry_at": null
  }
]

Disabling a webhook

If your endpoint is temporarily unavailable (e.g., during a deployment), you can pause delivery by setting the webhook status to disabled via PATCH /v1/webhooks/{webhookId}. Buildmarkets will not attempt delivery while the webhook is disabled.
Note: Buildmarkets does not queue events that occur while a webhook is disabled. Events that fire during a disabled window will not be retroactively delivered. Use the relevant Buildmarkets API endpoints to reconcile missed state changes if needed.

Security best practices

  • Verify every signature before processing any payload — do not skip this step even in development
  • Use HTTPS only — never register a webhook pointing to a plain HTTP URL
  • Store the secret securely — treat it like a password; use environment variables or a secrets manager, never hardcode it
  • Return 2xx fast, process async — offload event handling to a queue to avoid timeout-induced retries
  • Implement idempotency — use the event id to detect and discard duplicate deliveries
  • Monitor delivery history — set up alerts for sustained delivery failures so you can detect endpoint outages quickly

Next steps

Updated 3 months ago