Skip to content

Webhook Registration

Register Webhook to receive real-time transaction updates

  1. Log in to your account on the Zodia Markets Customer Portal
  2. Click on your profile icon in the top-right corner
  3. Select SETTINGS from the dropdown menu

Screenshot showing profile menu with Settings option highlighted


  1. In the Settings page, click the Notifications tab

  2. Add your webhook URL and click ‘Save’


Webhook URL Requirements:

  • Must use HTTPS or HTTP protocol
  • Must be publicly accessible
  • Must return responses within 10 seconds
  • Recommended: Use a dedicated endpoint (e.g., https://api.yourcompany.com/webhooks/zodia/transactions)

Zodia Markets sends webhook notifications as POST requests to your registered endpoint.

Request Method: POST

Request Headers:

Header
Content-Type: application/json

Request Body: The body contains transaction data in the same format as the Transaction List REST endpoint.

Sample:

{
"transactionClass": "RFSTRADE",
"uuid": "0650ae88-f6cb-11f0-bee0-8bfc65fb40ae",
"userUuid": "2073252c-81ed-41be-bf4d-d51b8f2246b8",
"amount": 14799.1,
"fee": 0,
"balanceBefore": 0,
"balanceAfter": 0,
"ccy": "HKD",
"transactionState": "PENDING",
"transactionType": "TRADE_CREDIT",
"received": 1769001230161,
"processed": 1769001230161,
"timestampMillis": 1769001230347,
"displayTitle": null,
"displayDescription": null,
"paymentTransferType": null,
"customRef": "407cbda6-0740-4ab4-8fc9-12ef40597048",
"quoteId": "AnNxW46hcgwHCivIJ8wGJ48UJb2GRn35dUXJQgg7HMDa6aR8ZGtyTcbb3IMBABQqHD0ud16e1AseExAYAzg2E0xAbzg7cXVbDwB6G4w4/kJI2S8soeprQNeorLBRHmzZO4Ef0R0hEnpijiNOaRIF6xVxZ0heDCYrcYxjqRw7OiddOz4UCGIYGTkKbTFlEyvyrTLmtlt91Y3vXQrR5CeXGAAGABsRBzl0BgAPOgc+PhcJAmoMAzk6NVQFHzoZLRAIGhMAWRgGLgQDOAAMRS0+dzQrdDkSKAAqRz4UFEM7EBgCOD53EDhhRgstdRATA2EuET4QWUM7FDVSOwEQDwApMQ8GKjk=_client",
"tradeId": "0650ae88-f6cb-11f0-bee0-8bfc65fb40ae",
"executedPrice": "[USDC/HKD] 7.78899821",
"tradeRef": "24e7091a9e114f83837e58e1256eb349_client",
"settleDate": "2026-01-22",
"beneficiaryUuid": null
}

Your webhook receives notifications when transactionState changes:

State Flow:

PENDING → PROCESSED

Example State Change:

Initial notification (PENDING):

{
"uuid": "0650ae88-f6cb-11f0-bee0-8bfc65fb40ae",
"transactionState": "PENDING",
"transactionClass": "RFSTRADE",
...
}

Update notification (PROCESSED):

{
"uuid": "0650ae88-f6cb-11f0-bee0-8bfc65fb40ae",
"transactionState": "PROCESSED",
"transactionClass": "RFSTRADE",
...
}

Your webhook receives updates for all transaction types:

Transaction Class Description Updates
OTCTRADE Trade legs entered by the Zodia Markets OTC desk. Each leg generates a separate notification (e.g., USDC buy leg and HKD sell leg) When trade leg moves from PENDING to PROCESSED
RFSTRADE Trade legs entered via e-Trader platform or WebSocket API. Each leg generates a separate notification (e.g., USDC buy leg and HKD sell leg) When trade leg moves from PENDING to PROCESSED
COIN Crypto deposit and withdrawal transactions When transaction moves from PENDING to PROCESSED
CASH Fiat deposit and withdrawal transactions When transaction moves from PENDING to PROCESSED
Transaction Type Description
TRADE_CREDIT Credit side of a trade (receiving asset)
TRADE_DEBIT Debit side of a trade (sending asset)
DEPOSIT Incoming deposit (crypto or fiat)
WITHDRAWAL Outgoing withdrawal (crypto or fiat)

All webhook notifications are sent from a fixed set of static IP addresses. You can whitelist these IPs on your firewall or WAF to ensure you only accept traffic from Zodia Markets.

  • 18.175.43.216
  • 35.177.190.134

Rather than hardcoding IPs, we recommend fetching them programmatically from our published endpoint:

GET https://trade.zodiamarkets.com/.well-known/webhooks/ips.json

Response:

{
"service": "zodia-markets-ips",
"version": 1,
"ipv4_cidrs": [
"18.175.43.216/32",
"35.177.190.134/32"
],
"created_at": "2026-03-10T00:00:00Z",
"last_updated": "2026-03-10T00:00:00Z",
"change_notice": null
}

Fields:

Field Description
version Integer that increments whenever the IP list changes. Monitor this to detect updates.
ipv4_cidrs List of source IP addresses in CIDR notation (/32 = single IP).
last_updated Timestamp of the most recent change to the IP list.
change_notice null when no changes are planned. When an IP change is scheduled, this will contain a message and effective_date.

IP Change Policy: Zodia Markets will provide a minimum of 30 days’ advance notice before any change to the webhook source IPs. During transitions, both old and new IPs will be active simultaneously. The change_notice field and notifications will communicate upcoming changes.

Your webhook endpoint must return a successful HTTP status code to acknowledge receipt.

Success Status Codes:

  • 200 OK (recommended)
  • 201 Created
  • 202 Accepted
  • 204 No Content

Response Body: Optional (can be empty)

Example Success Response:

HTTP/1.1 200 OK
Content-Type: application/json
{
"received": true,
"transactionId": "0650ae88-f6cb-11f0-bee0-8bfc65fb40ae"
}

If your webhook endpoint returns an error or times out, Zodia Markets will retry the notification.

Error Status Codes:

  • 4xx (Client Error) - No retry
  • 5xx (Server Error) - Will retry with exponential backoff
  • Timeout (>10 seconds) - Will retry max 3 times if webhook service endpoint is available

Idempotency: Your webhook may receive the same notification multiple times. Implement idempotent processing using the uuid field.


# Track processed transaction UUIDs
processed_transactions = set()
def process_webhook(transaction):
transaction_uuid = transaction['uuid']
if transaction_uuid in processed_transactions:
print(f"Duplicate notification for {transaction_uuid}, skipping")
return
# Process transaction
handle_transaction(transaction)
# Mark as processed
processed_transactions.add(transaction_uuid)

Only process transactions that reach PROCESSED state:

def process_webhook(transaction):
if transaction['transactionState'] != 'PROCESSED':
print(f"Transaction {transaction['uuid']} still pending, skipping")
return
# Process completed transaction
handle_processed_transaction(transaction)

Check for beneficiary UUID to identify third-party settlements:

def process_webhook(transaction):
if transaction.get('beneficiaryUuid'):
print(f"Third-party settlement detected")
print(f"Beneficiary: {transaction['beneficiaryUuid']}")
handle_third_party_settlement(transaction)
else:
print(f"Standard settlement to own account")
handle_standard_settlement(transaction)
import logging
@app.route('/webhook', methods=['POST'])
def webhook():
transaction = request.json
# Log incoming webhook
logging.info(f"Webhook received: {transaction['uuid']}")
logging.info(f"Class: {transaction['transactionClass']}")
logging.info(f"State: {transaction['transactionState']}")
logging.info(f"Amount: {transaction['amount']} {transaction['ccy']}")
try:
process_webhook(transaction)
logging.info(f"Webhook processed successfully: {transaction['uuid']}")
except Exception as e:
logging.error(f"Webhook processing failed: {e}")
raise
return jsonify({'received': True}), 200