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. PENDING is the initial value; PROCESSED, REVERSED and CANCELLED are the terminal values a later notification can carry — the same four states documented in the Transaction List endpoint’s transactionState table. REVERSED (a leg of the transaction has been reversed) and CANCELLED (settlement has been cancelled) both mean the transfer did not complete.

State Flow:

PENDING → PROCESSED
PENDING → REVERSED
PENDING → CANCELLED

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",
...
}

Update notification (REVERSED):

{
"uuid": "0650ae88-f6cb-11f0-bee0-8bfc65fb40ae",
"transactionState": "REVERSED",
"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 transactionState changes
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 transactionState changes
COIN Crypto deposit and withdrawal transactions When transaction transactionState changes
CASH Fiat deposit and withdrawal transactions When transaction transactionState changes
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)

transactionState is PENDING while the transaction is in flight. PROCESSED, REVERSED and CANCELLED are the terminal values — do not treat “not PROCESSED” as “still pending” and skip it: REVERSED and CANCELLED both mean the transfer did not complete, and need handling of their own.

def process_webhook(transaction):
state = transaction['transactionState']
if state == 'PENDING':
print(f"Transaction {transaction['uuid']} still pending, skipping")
return
if state == 'PROCESSED':
handle_processed_transaction(transaction)
elif state in ('REVERSED', 'CANCELLED'):
# The transfer did not complete: handle it, do not discard it.
handle_incomplete_transaction(transaction)
else:
print(f"Unrecognised transactionState: {state}")

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