Withdrawals - Reconciliation

Platforms who manage a ledger on their end must abide by our conformance requirements

Context

Within the larger Account Funding product, Zero Hash offers the ability for Platforms to allow their end customers to make crypto and stablecoin withdrawals. The integration guide for the withdrawal flow is here. This page is targeted, aiming to provide clarity and instructions for a successful integration and to reduce reconciliation risk.

Most Platforms will manage the fiat ledger on their end. For example, consider a brokerage platform that allows its end customers to fund their accounts and make withdrawals using stablecoins. On the deposit, Zero Hash will initially credit the user with USDC. We'll then convert to USD in the customer account and transfer those funds to the brokerage's account. At this point, Zero Hash transfers control of the end customer's account balance (ie, the ledger) to the brokerage. So when withdrawals are made, Zero Hash does not have the ability to perform an informed credit check.

The Platform is completely responsible for ensuring that the end customer does not withdraw more than their current balance.

Let's now look at the end-to-end flow, where certain Requirements are stated

High level flow

  1. Platform invokes Withdraw SDK
  2. End Customer initiates withdraw
  3. Zero Hash sends webhooks to Platform

1. Platform invokes Withdraw SDK

As a refresher, when invoking the Withdraw SDK, the Platform must specify the following fields:

  • external_account_id
  • quoted_asset
  • amount

The Platform can optionally specify a reference_id, which is a free-form field that the Platform can use to assist with matching the Zero Hash withdrawal status with the Platform transaction status. Zero Hash will add the reference_id on all subsequent webhook messages related to this withdrawal attempt. Example POST /client_auth_token request:

 {
    "participant_code": "CUST01",
    "permissions": ["crypto-withdrawals"],
    "withdrawal_details": {
        "external_account_id": "c476a81f-a29f-4e22-88db-1f521d7cf004",
        "quoted_asset": "USD",   
        "withdrawal_request_amount": "200"
    },
   "reference_id": "0bd7f7f0-cf26-495f-b2df-e8afe8481ba3"
}

Requirement 1: The Platform must encumber the funds (in other words, "lock") the funds on their system, preventing the end customer from using those funds until the withdrawal is in a terminal state.

There are 2 possible terminal states:

  • settled - the withdrawal completed and the Platform can assume that the End Customer has its funds. The Platform needs to settle the outstanding amount with Zero Hash on the settlement cadence originally agreed upon.
  • failed - the Platform may re-credit the end customer's balance with the amount. This is the only state where the Platform may return funds back to the end customer

2. End Customer initiates withdraw

The End Customer will click the Initiate withdrawal button on the Confirm Withdrawal details screen of the Withdraw SDK, which will trigger the submitted webhook message.

3. Zero Hash sends webhooks to the Platform

Zero Hash will send a series of webhooks that correspond to the status of the withdrawal:

  • submitted - Zero Hash has received the withdrawal request
  • pending - Zero Hash is processing the request
  • posted - The withdrawal has been sent on-chain
  • settled - The withdrawal has completed on-chain (the End Customer has their funds)
  • failed - The withdrawal was not successfully sent on-chain (the End Customer does not have their funds)

Requirement 2: The Platform must keep track of withdrawal states on their end. In order to progress the transaction through the state machine, the webhooks have to be matched per the instructions in the "Field matching logic" section below. When building your state machine, we recommend you persist Zero Hash's unique ID, the payment_id as well. You can use this ID to self-serve the record via the GET /payments/{payment_id} REST endpoint.

Requirement 3: If there is ever a case where a webhook cannot be matched (or otherwise "processed successfully") by the Platform, the funds that are encumbered must continue to encumbered

We strongly encourage that the Platform set up alert automation to make your systems and people aware of a withdrawal that was unsuccessfully processed. The next step is to reach out to Zero Hash support - our operations team will assist in reconciling the state.

Field matching logic

To consider a withdrawal as "matched", the Platform's system must be able to perform a one-to-one match between the following field values included in the webhook message and the corresponding records on the Platform's side:

  • [If reference_id is not supplied by the Platform] participant_code andwithdrawal_request_amount
  • [If reference_id is supplied by the Platform] participant_code, withdrawal_request_amount and reference_id

We strongly encourage that the Platform only allow 1 withdrawal at a time per participant_code. If you choose to allow more than one, the Platform should use created_at to disambiguate multiple open withdrawal requests for the same participant_code. For example, the timestamp that the Platform invoked the SDK originally should be "close" the the created_at value.

Scenarios

Each Platform should account for the following scenarios before going live.

Successful withdrawal

  • The Platform invokes the SDK with the following request:
 {
    "participant_code": "CUST01",
    "permissions": ["crypto-withdrawals"],
    "withdrawal_details": {
        "external_account_id": "c476a81f-a29f-4e22-88db-1f521d7cf004",
        "quoted_asset": "USD",   
        "withdrawal_request_amount": "200"
    },
   "reference_id": "0bd7f7f0-cf26-495f-b2df-e8afe8481ba3"
}
  • End customer clicks the Initiate withdrawal on the Confirm Withdrawal details screen
  • Zero Hash sends the following webhooks:

submitted

{
   "payment_id":"0f68333e-2114-469d-b505-c850d776e061",
   "obo_participant":{
      "participant_code":"CUST01",
      "account_group":"PLAT01",
      "account_label":"general"
   },
   "payment_details":{
      "withdrawal_request_id":"",
      "trade_id":"b752503c-1c42-4dfe-ad1d-7b39da5db59c",
      "on_chain_transaction_id":"",
      "network_fee_notional":"",
      "network_fee_quantity":"",
      "withdrawal_fee_notional": "",
      "destination_address":"0xa6b0Cd1baaa15AE97D8135f0E87F61af27c6cB89"
   },
   "asset":"USDC",
   "network":"ETH",
   "payment_type":"withdrawal",
   "external_account_id":"c476a81f-a29f-4e22-88db-1f521d7cf004",
   "participant_code":"CUST01",
   "quantity":"",
   "status":"submitted",
   "created_at":"2024-09-26T13:05:22.657Z",
   "updated_at":"2024-09-26T13:05:22.657Z",
   "total":"200",
   "reference_id": "0bd7f7f0-cf26-495f-b2df-e8afe8481ba3"
}

→ The Platform updates their state machine for this transaction to submitted

pending

{
   "payment_id":"0f68333e-2114-469d-b505-c850d776e061",
   "obo_participant":{
      "participant_code":"CUST01",
      "account_group":"PLAT01",
      "account_label":"general"
   },
   "payment_details":{
      "withdrawal_request_id":"",
      "trade_id":"b752503c-1c42-4dfe-ad1d-7b39da5db59c",
      "on_chain_transaction_id":"",
      "network_fee_notional":"",
      "network_fee_quantity":"",
      "withdrawal_fee_notional": "",
      "destination_address":"0xa6b0Cd1baaa15AE97D8135f0E87F61af27c6cB89"
   },
   "asset":"USDC",
   "network":"ETH",
   "payment_type":"withdrawal",
   "external_account_id":"c476a81f-a29f-4e22-88db-1f521d7cf004",
   "participant_code":"CUST01",
   "quantity":"",
   "status":"pending",
   "created_at":"2024-09-26T13:05:22.657Z",
   "updated_at":"2024-09-26T13:05:22.657Z",
   "total":"200",
   "reference_id": "0bd7f7f0-cf26-495f-b2df-e8afe8481ba3"
}

→ The Platform updates their state machine for this transaction to pending

posted

{
   "payment_id":"0f68333e-2114-469d-b505-c850d776e061",
   "obo_participant":{
      "participant_code":"CUST01",
      "account_group":"PLAT01",
      "account_label":"general"
   },
   "payment_details":{
      "withdrawal_request_id":"14f8ebb8-7530-4aa4-bef9-9d73d56313f3",
      "trade_id":"b752503c-1c42-4dfe-ad1d-7b39da5db59c",
      "on_chain_transaction_id":"0x55dfac6137387a81e32fc353fca45eea3124cd42564a4112192323add8dee1da",
      "network_fee_notional":"1.25",
      "network_fee_quantity":".00032",
      "withdrawal_fee_notional": "1.50",
      "destination_address":"0xa6b0Cd1baaa15AE97D8135f0E87F61af27c6cB89",
   "asset":"USDC",
   "network":"ETH",
   "payment_type":"withdrawal",
   "external_account_id":"c476a81f-a29f-4e22-88db-1f521d7cf004",
   "participant_code":"CUST01",
   "quantity":"197.25",
   "status":"posted",
   "created_at":"2024-09-26T13:05:22.657Z",
   "updated_at":"2024-09-26T13:05:22.657Z",
   "total":"200",
   "reference_id": "0bd7f7f0-cf26-495f-b2df-e8afe8481ba3"
}  

→ The Platform updates their state machine for this transaction to posted

settled

{
   "payment_id":"0f68333e-2114-469d-b505-c850d776e061",
   "obo_participant":{
      "participant_code":"CUST01",
      "account_group":"PLAT01",
      "account_label":"general"
   },
   "payment_details":{
      "withdrawal_request_id":"14f8ebb8-7530-4aa4-bef9-9d73d56313f3",
      "trade_id":"b752503c-1c42-4dfe-ad1d-7b39da5db59c",
      "on_chain_transaction_id":"0x55dfac6137387a81e32fc353fca45eea3124cd42564a4112192323add8dee1da",
      "network_fee_notional":"1.25",
      "network_fee_quantity":".00032",
      "withdrawal_fee_notional": "1.50",
      "destination_address":"0xa6b0Cd1baaa15AE97D8135f0E87F61af27c6cB89"
   },
   "asset":"USDC",
   "network":"ETH",
   "payment_type":"withdrawal",
   "external_account_id":"c476a81f-a29f-4e22-88db-1f521d7cf004",
   "participant_code":"CUST01",
   "quantity":"197.25",
   "status":"settled",
   "created_at":"2024-09-26T13:05:22.657Z",
   "updated_at":"2024-09-26T13:05:22.657Z",
   "total":"200",
   "reference_id": "0bd7f7f0-cf26-495f-b2df-e8afe8481ba3"
}  

→ The Platform updates their state machine for this transaction to settled and importantly, ensures that the end customer balances remains deducted by the withdrawal amount.

Failed Withdrawal - Bad state transition

  • The Platform invokes the SDK with the following request:
 {
    "participant_code": "CUST01",
    "permissions": ["crypto-withdrawals"],
    "withdrawal_details": {
        "external_account_id": "c476a81f-a29f-4e22-88db-1f521d7cf004",
        "quoted_asset": "USD",   
        "withdrawal_request_amount": "200"
    },
   "reference_id": "0bd7f7f0-cf26-495f-b2df-e8afe8481ba3"
}
  • End customer clicks the Initiate withdrawal on the Confirm Withdrawal details screen
  • Zero Hash sends the following webhooks:

submitted

{
   "payment_id":"0f68333e-2114-469d-b505-c850d776e061",
   "obo_participant":{
      "participant_code":"CUST01",
      "account_group":"PLAT01",
      "account_label":"general"
   },
   "payment_details":{
      "withdrawal_request_id":"",
      "trade_id":"b752503c-1c42-4dfe-ad1d-7b39da5db59c",
      "on_chain_transaction_id":"",
      "network_fee_notional":"",
      "network_fee_quantity":"",
      "withdrawal_fee_notional": "",
      "destination_address":"0xa6b0Cd1baaa15AE97D8135f0E87F61af27c6cB89"
   },
   "asset":"USDC",
   "network":"ETH",
   "payment_type":"withdrawal",
   "external_account_id":"c476a81f-a29f-4e22-88db-1f521d7cf004",
   "participant_code":"CUST01",
   "quantity":"",
   "status":"submitted",
   "created_at":"2024-09-26T13:05:22.657Z",
   "updated_at":"2024-09-26T13:05:22.657Z",
   "total":"200",
   "reference_id": "0bd7f7f0-cf26-495f-b2df-e8afe8481ba3"
}

→ The Platform updates their state machine for this transaction to submitted

pending

The Platform does not receive a pending webhook

→ The Platform does not update their state machine

posted

{
   "payment_id":"0f68333e-2114-469d-b505-c850d776e061",
   "obo_participant":{
      "participant_code":"CUST01",
      "account_group":"PLAT01",
      "account_label":"general"
   },
   "payment_details":{
      "withdrawal_request_id":"14f8ebb8-7530-4aa4-bef9-9d73d56313f3",
      "trade_id":"b752503c-1c42-4dfe-ad1d-7b39da5db59c",
      "on_chain_transaction_id":"0x55dfac6137387a81e32fc353fca45eea3124cd42564a4112192323add8dee1da",
      "network_fee_notional":"1.25",
      "network_fee_quantity":".00032",
      "withdrawal_fee_notional": "1.50",
      "destination_address":"0xa6b0Cd1baaa15AE97D8135f0E87F61af27c6cB89",
   "asset":"USDC",
   "network":"ETH",
   "payment_type":"withdrawal",
   "external_account_id":"c476a81f-a29f-4e22-88db-1f521d7cf004",
   "participant_code":"CUST01",
   "quantity":"197.25",
   "status":"posted",
   "created_at":"2024-09-26T13:05:22.657Z",
   "updated_at":"2024-09-26T13:05:22.657Z",
   "total":"200",
   "reference_id": "0bd7f7f0-cf26-495f-b2df-e8afe8481ba3"
}  

At this point, the Platform realizes there is an issue - the pending status was not recognized by the Platform. An alert that the Platform set up is triggered.. The next step that we recommend is to take the payment_id (Zero Hash's unique id for this transaction) and use that in the GET /payments/{payment_id} API call to attempt to get back to reconciliation. Given the matching logic stands true, you can use this endpoint to see the most recent status of the withdrawal.

Successful withdrawal - matching logic failure

  • The Platform invokes the SDK with the following request:
 {
    "participant_code": "CUST01",
    "permissions": ["crypto-withdrawals"],
    "withdrawal_details": {
        "external_account_id": "c476a81f-a29f-4e22-88db-1f521d7cf004",
        "quoted_asset": "USD",   
        "withdrawal_request_amount": "200"
    },
   "reference_id": "0bd7f7f0-cf26-495f-b2df-e8afe8481ba3"
}
  • End customer clicks the Initiate withdrawal on the Confirm Withdrawal details screen
  • Zero Hash sends the following webhooks:

submitted

{
   "payment_id":"0f68333e-2114-469d-b505-c850d776e061",
   "obo_participant":{
      "participant_code":"CUST01",
      "account_group":"PLAT01",
      "account_label":"general"
   },
   "payment_details":{
      "withdrawal_request_id":"",
      "trade_id":"b752503c-1c42-4dfe-ad1d-7b39da5db59c",
      "on_chain_transaction_id":"",
      "network_fee_notional":"",
      "network_fee_quantity":"",
      "withdrawal_fee_notional": "",
      "destination_address":"0xa6b0Cd1baaa15AE97D8135f0E87F61af27c6cB89"
   },
   "asset":"USDC",
   "network":"ETH",
   "payment_type":"withdrawal",
   "external_account_id":"c476a81f-a29f-4e22-88db-1f521d7cf004",
   "participant_code":"CUST01",
   "quantity":"",
   "status":"submitted",
   "created_at":"2024-09-26T13:05:22.657Z",
   "updated_at":"2024-09-26T13:05:22.657Z",
   "total":"200",
   "reference_id": "0bd7f7f0-cf26-495f-b2df-e8afe8481ba3"
}

→ The Platform updates their state machine for this transaction to submitted

pending

{
   "payment_id":"0f68333e-2114-469d-b505-c850d776e061",
   "obo_participant":{
      "participant_code":"CUST01",
      "account_group":"PLAT01",
      "account_label":"general"
   },
   "payment_details":{
      "withdrawal_request_id":"",
      "trade_id":"b752503c-1c42-4dfe-ad1d-7b39da5db59c",
      "on_chain_transaction_id":"",
      "network_fee_notional":"",
      "network_fee_quantity":"",
      "withdrawal_fee_notional": "",
      "destination_address":"0xa6b0Cd1baaa15AE97D8135f0E87F61af27c6cB89"
   },
   "asset":"USDC",
   "network":"ETH",
   "payment_type":"withdrawal",
   "external_account_id":"c476a81f-a29f-4e22-88db-1f521d7cf004",
   "participant_code":"CUST01",
   "quantity":"",
   "status":"pending",
   "created_at":"2024-09-26T13:05:22.657Z",
   "updated_at":"2024-09-26T13:05:22.657Z",
   "total":"200",
   "reference_id": "" <-- NULL
}

→ Despite the null reference_id, the Platform will be able to determine that this withdrawal indeed went through, as the total, participant_code, external_account_id all match with the original transaction details.

Failed withdrawal - on-chain processing failed

  • The Platform invokes the SDK with the following request:
 {
    "participant_code": "CUST01",
    "permissions": ["crypto-withdrawals"],
    "withdrawal_details": {
        "external_account_id": "c476a81f-a29f-4e22-88db-1f521d7cf004",
        "quoted_asset": "USD",   
        "withdrawal_request_amount": "200"
    },
   "reference_id": "0bd7f7f0-cf26-495f-b2df-e8afe8481ba3"
}
  • End customer clicks the Initiate withdrawal on the Confirm Withdrawal details screen
  • Zero Hash sends the following webhooks:

submitted

{
   "payment_id":"0f68333e-2114-469d-b505-c850d776e061",
   "obo_participant":{
      "participant_code":"CUST01",
      "account_group":"PLAT01",
      "account_label":"general"
   },
   "payment_details":{
      "withdrawal_request_id":"",
      "trade_id":"b752503c-1c42-4dfe-ad1d-7b39da5db59c",
      "on_chain_transaction_id":"",
      "network_fee_notional":"",
      "network_fee_quantity":"",
      "withdrawal_fee_notional": "",
      "destination_address":"0xa6b0Cd1baaa15AE97D8135f0E87F61af27c6cB89"
   },
   "asset":"USDC",
   "network":"ETH",
   "payment_type":"withdrawal",
   "external_account_id":"c476a81f-a29f-4e22-88db-1f521d7cf004",
   "participant_code":"CUST01",
   "quantity":"",
   "status":"submitted",
   "created_at":"2024-09-26T13:05:22.657Z",
   "updated_at":"2024-09-26T13:05:22.657Z",
   "total":"200",
   "reference_id": "0bd7f7f0-cf26-495f-b2df-e8afe8481ba3"
}

→ The Platform updates their state machine for this transaction to submitted

pending

{
   "payment_id":"0f68333e-2114-469d-b505-c850d776e061",
   "obo_participant":{
      "participant_code":"CUST01",
      "account_group":"PLAT01",
      "account_label":"general"
   },
   "payment_details":{
      "withdrawal_request_id":"",
      "trade_id":"b752503c-1c42-4dfe-ad1d-7b39da5db59c",
      "on_chain_transaction_id":"",
      "network_fee_notional":"",
      "network_fee_quantity":"",
      "withdrawal_fee_notional": "",
      "destination_address":"0xa6b0Cd1baaa15AE97D8135f0E87F61af27c6cB89"
   },
   "asset":"USDC",
   "network":"ETH",
   "payment_type":"withdrawal",
   "external_account_id":"c476a81f-a29f-4e22-88db-1f521d7cf004",
   "participant_code":"CUST01",
   "quantity":"",
   "status":"pending",
   "created_at":"2024-09-26T13:05:22.657Z",
   "updated_at":"2024-09-26T13:05:22.657Z",
   "total":"200",
   "reference_id":"0bd7f7f0-cf26-495f-b2df-e8afe8481ba3"
}

→ The Platform updates their state machine for this transaction to pending

posted

{
   "payment_id":"0f68333e-2114-469d-b505-c850d776e061",
   "obo_participant":{
      "participant_code":"CUST01",
      "account_group":"PLAT01",
      "account_label":"general"
   },
   "payment_details":{
      "withdrawal_request_id":"14f8ebb8-7530-4aa4-bef9-9d73d56313f3",
      "trade_id":"b752503c-1c42-4dfe-ad1d-7b39da5db59c",
      "on_chain_transaction_id":"0x55dfac6137387a81e32fc353fca45eea3124cd42564a4112192323add8dee1da",
      "network_fee_notional":"1.25",
      "network_fee_quantity":".00032",
      "withdrawal_fee_notional": "1.50",
      "destination_address":"0xa6b0Cd1baaa15AE97D8135f0E87F61af27c6cB89",
   "asset":"USDC",
   "network":"ETH",
   "payment_type":"withdrawal",
   "external_account_id":"c476a81f-a29f-4e22-88db-1f521d7cf004",
   "participant_code":"CUST01",
   "quantity":"197.25",
   "status":"posted",
   "created_at":"2024-09-26T13:05:22.657Z",
   "updated_at":"2024-09-26T13:05:22.657Z",
   "total":"200",
   "reference_id": "0bd7f7f0-cf26-495f-b2df-e8afe8481ba3"
}  

→ The Platform updates their state machine for this transaction to posted

failed

{
   "payment_id":"0f68333e-2114-469d-b505-c850d776e061",
   "obo_participant":{
      "participant_code":"CUST01",
      "account_group":"PLAT01",
      "account_label":"general"
   },
   "payment_details":{
      "withdrawal_request_id":"14f8ebb8-7530-4aa4-bef9-9d73d56313f3",
      "trade_id":"b752503c-1c42-4dfe-ad1d-7b39da5db59c",
      "on_chain_transaction_id":"0x55dfac6137387a81e32fc353fca45eea3124cd42564a4112192323add8dee1da",
      "network_fee_notional":"1.25",
      "network_fee_quantity":".00032",
      "withdrawal_fee_notional": "1.50",
      "destination_address":"0xa6b0Cd1baaa15AE97D8135f0E87F61af27c6cB89",
   "asset":"USDC",
   "network":"ETH",
   "payment_type":"withdrawal",
   "external_account_id":"c476a81f-a29f-4e22-88db-1f521d7cf004",
   "participant_code":"CUST01",
   "quantity":"197.25",
   "status":"failed",
   "created_at":"2024-09-26T13:05:22.657Z",
   "updated_at":"2024-09-26T13:05:22.657Z",
   "total":"200",
   "reference_id": "0bd7f7f0-cf26-495f-b2df-e8afe8481ba3"
}  

→ The blockchain was down, and the Customer was unable to receive it's withdrawal. The Platform will give the funds back to the Customer on their ledger.