Webhooks
Currently, a return webhook is configured by contacting Zero Hash directly. Please reach out via the platform Slack channel, or directly to your Zero Hash relationship manager, with the URLs to direct production (Prod) and certification (Cert) webhooks. Webhooks are configured within 2 business days of receiving the URL.
Participant status changes
Platforms leveraging Zero Hash’s customer verification product should subscribe to webhooks to know if participants have been approved or rejected. This webhook can also be used by all platforms to understand if participants’ statuses have changed (i.e. if the participant has been locked or disabled).
Once a platform is set up to receive participant status webhooks, the payload is a JSON object containing the following fields:
Parameter | Description | Type |
---|---|---|
participant_code | The Zero Hash identifier for the new customer Note: this value is key to enable you to submit trades and check account balances for this customer | string |
participant_status | The status of the participant, which dictates whether they can trade or not. e.g. submitted, approved, rejected, locked, disabled, divested, closed | string |
reason_code | If applicable, the reason the participant is in a status of locked, disabled, closed, or divested; e.g. compliance_issue, user_request. On rejection, this is set to reject_reasons[0] | string |
reject_reasons | If applicable, the reason(s) that a participant could have failed KYC verification. This is currently only applicable to participant rejections. | []string |
timestamp | The UNIX timestamp (in milliseconds) representing when the status was changed | timestamp |
Examples
Participant submitted
{
"participant_code": "ABC123",
"participant_status": "submitted",
"timestamp": 1670958435349
}
Participant approved
{
"participant_code": "ABC123",
"participant_status": "approved",
"timestamp": 1670958435349
}
Verification failed
{
"participant_code": "ABC123",
"participant_status": "rejected",
"reason_code": "kyc.idv.name_not_extracted",
"timestamp": 1708489569494,
"reject_reasons": [
"kyc.idv.name_not_extracted",
"kyc.pii.national_id_not_present"
]
}
Participant status change
{
"participant_code": "ABC123",
"participant_status": "locked",
"reason_code": "compliance_issue",
"timestamp": 1670958435349
}
Payments status changes
Platforms leveraging Zero Hash’s payments product should subscribe to webhooks to know if payments statuses have changed.
Once a platform is set up to receive payment status webhooks, the payload is a JSON object containing the following fields:
Parameter | Description | Type |
---|---|---|
participant_code | The Zero Hash identifier for the customer requesting an ACH transaction. | string |
type | Indicates if the payment is a credit or a debit for the end customer | string |
transaction_id | The unique identifier generated by Zero Hash for the transaction. | string |
payment_status | The current status of the payment. See payment statuses for more. | string |
reason_code | The NACHA failure reason code, if the payment_status is returned. | string |
reason_description | The description matching the reason_code, if the payment_status is returned. | string |
expected_settlement_date | For payment type of credit, when the funds are expected to settle to the end customer bank account. This is an approximation as receiving banks are ultimately responsible for posting the update. | string; YYYY-MM-DD format |
trade_id | The Zero Hash identifier for the trade associated to the ACH transaction. | string |
trade_status | The status of the trade idenfied by trade_id . |
string |
rejected_reason | The reason why the transaction was rejected (if the transaction status is 'rejected') | string |
Examples
ACH debit
{
"participant_code": "ABC123",
"type": "debit",
"transaction_id": "e8641f4b-2098-4f86-95ba-711151cee6a5",
"payment_status": "settled"
}
ACH credit
{
"participant_code": "ABC123",
"type": "credit",
"transaction_id": "e8641f4b-2098-4f86-95ba-711151cee6a9",
"payment_status": "posted",
"expected_settlement_date": "2024-03-01"
}
ACH return
{
"participant_code": "ABC123",
"type": "debit",
"transaction_id": "e8641f4b-2098-4f86-95ba-711151cee6a5",
"payment_status": "returned",
"reason_code": "R01",
"reason_description": "Insufficient funds"
}
ACH rejected
{
"participant_code":"ABC123",
"type":"debit",
"transaction_id":"0220358e-6053-43e1-9f94-bb1a09ff2fc7",
"payment_status":"rejected",
"rejected_reason":"max_transaction_amount(max_transaction_amount_per_transaction)"
}
External Accounts status changes
Platforms leveraging Zero Hash’s payments product should subscribe to webhooks to know if an external account statuses have changed.
Once a platform is set up to receive external account status webhooks, the payload is a JSON object containing the following fields:
Parameter | Description | Type |
---|---|---|
participant_code | The Zero Hash identifier for the customer owner of the external account. | string |
account_nickname | Nickname of the account stablished by the client. | string |
account_type | Type of the account ('checking', 'savings'). | string |
external_account_id | The Zero Hash identifier for the external account. | string |
external_account_status | The status of the external account. See external account statuses for more. | string |
timestamp | The UNIX timestamp (in milliseconds) representing when the status was changed. | timestamp |
In addition to the JSON body, we also include headers:
Name | Description |
---|---|
x-zh-hook-notification-id | Notification ID, can be used for idempotency checks |
x-zh-hook-payload-type | Payload type string |
- The value of should
x-zh-hook-payload-type
beparticipant_status_changed
for this use of webhooks.
Depending on your security configuration, additional headers may also be included:
Name | Description |
---|---|
x-zh-hook-signature-256 | to_hex(hmac(sha_256(payload), your-secret)) |
x-zh-hook-rsa-signature-256 | to_hex(rsa(sha_256(payload), zh-sec-key)) |
Zero Hash’s webhook requests will originate from the following IP addresses, should you need an allow-list:
18.189.25.175/32
3.18.218.32/32
3.22.145.85/32
Fund
Platform’s leveraging the Fund endpoints can subscribe to webhooks in order to be made aware that a Fund event has completed. This can be used to update internal monitoring tools or to update your user-facing front end.
Once a platform is set up to receive external account status webhooks, the payload is a JSON object containing the following fields:
Parameter | Description | Type |
---|---|---|
participant_code | End customer's participant code | string |
fund_asset | The crypto asset that was sent by the end customer in order to fund an account | string |
rate | The rate at which the fund_asset was liquidated at | string |
quoted_currency | The asset that the provided funding_rate was denominated in, and will ultimately be converted into automatically upon a deposit | string |
deposit_address | The address that the end customer deposited the crypto asset to | string |
quantity | The amount, denominated in the fund_asset, that was funded | string |
notional | The amount, denominated in the quoted_currency, that was funded | string |
fund_id | The Zero Hash-generated unique identifier associated with a executed fund event | string |
fund_timestamp | The UNIX timestamp of when the fund event was completed | number |
transaction_id | The on-chain transaction id associated with the deposit | string |
account_label | The account_label where the deposit was credited to | string |
Example
{
"participant_code": "CUST01",
"fund_asset": "USDC",
"rate": "1",
"quoted_currency": "USD",
"deposit_address": "0x3A45a60c635E6cD616B1C4510404Eba88116050C",
"quantity": "500",
"notional": "500",
"fund_id": "5155f7c9-95cb-4556-ab89-c178943a7111",
"fund_timestamp": 1550174574,
"transaction_id": "a07407e8f98c21b037b4aa0cbc852b8489c5e122fcc3d4b33b7827d0605ad8ff",
"account_label": "general"
}
Webhook Security
RSA security method (recommended)
This is Zero Hash’s recommended form of webhook security as it does not require passing secret keys back and forth.
Full example available at Link to Go playground.
golang
import (
"crypto"
"crypto/rsa"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"net/http"
)
func verifySignature(r *http.Request, pub *rsa.PublicKey) error {
// Calculate payload sha256 hash.
hash, err := calculatePayloadHash(r)
if err != nil {
return fmt.Errorf("calculate hash: %w", err)
}
// Get request signatue, signature is hex encoded.
const zhWebhookRSASecHeader = "x-zh-hook-rsa-signature-256"
requestSignature, err := hex.DecodeString(r.Header.Get(zhWebhookRSASecHeader))
if err != nil {
return fmt.Errorf("decode header: %w", err)
}
// Verify signatiure
err = rsa.VerifyPSS(pub, crypto.SHA256, hash, requestSignature, nil)
if err != nil {
return fmt.Errorf("verify PSS: %w", err)
}
return nil
}
func calculatePayloadHash(r *http.Request) ([]byte, error) {
payload, err := io.ReadAll(r.Body)
if err != nil {
return nil, fmt.Errorf("read body: %w", err)
}
h := sha256.New()
_, err = h.Write(payload)
if err != nil {
return nil, fmt.Errorf("write sha buffer: %w", err)
}
return h.Sum(nil), nil
}
Secret token method
To ensure the authenticity of the webhook, you may provide a secret token to us during configuration. If you’ve done so, we will include the x-zh-hook-signature-256 header with the webhook.
Samples
Python
import hashlib
import hmac
def verify_signature(payload_body, secret_token, signature_header) -> bool:
hash_object = hmac.new(secret_token.encode('utf-8'), msg=payload_body, digestmod=hashlib.sha256)
expected_signature = hash_object.hexdigest()
return hmac.compare_digest(expected_signature, signature_header)
TypeScript
import * as crypto from "crypto";
const WEBHOOK_SECRET: string = process.env.WEBHOOK_SECRET;
const verify_signature = (payload_body, secret_token, signature_header) => {
const signature = crypto
.createHmac("sha256", secret_token)
.update(JSON.stringify(payload_body))
.digest("hex");
return `{signature}` === signature_header;
};