NAV Navbar

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

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:

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

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;
};