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
- Platform invokes Withdraw SDK
- End Customer initiates withdraw
- 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 theamount
. 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 requestpending
- Zero Hash is processing the requestposted
- The withdrawal has been sent on-chainsettled
- 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
andreference_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.
Updated 2 days ago