Funding Models
zerohash supports three ACH funding models: Pre-funded, On-demand, and Instant USD. The model you choose determines funds availability for your customers and balance requirements you may have to begin processing.
Three Funding Models
You will have a choice between our three available funding models:
| Funding Model | Explanation | Float? | Loss Reserve? |
|---|---|---|---|
| On-demand | Trades execute instantly once the ACH debit is approved (buy) or sale is initiated (sell), rather than waiting for settlement. A platform-funded float enables this, with your float balance determining how much your customers can trade. | ✅ | ✅ |
| Pre-funded | Customers must fund their account before trading. Fund availability occurs after ACH settlement plus a brief holding period to account for ACH return risk. Withdrawals work the opposite way: customers sell crypto, then they can withdraw from their USD balance. | ❌ | ✅ |
| Instant USD | Customers USD balances are credited immediately upon ACH debit approval. They can trade with those USD balances before the ACH settles. Platform float funds the balances and determines how much your customers can trade. | ✅ | ✅ |
Compatibility of funding models, liquidity models and trading engines:
| Funding Model | CLOB? | RFQ + Novated? | RFQ + Direct? |
|---|---|---|---|
| On-demand | ✅ | ❌ | ✅ |
| Pre-funded | ❌ | ✅ | ✅ |
| Instant USD | ✅ | ❌ | ✅ |
On-demand
On-demand Integration Guide
After you Link a bank account, you can request crypto buys and sells. Crypto buys debit the end customer bank account, while crypto sells credit the end customer bank account. The platform float is leveraged to exchange crypto while the fiat leg of the transaction settles.
ℹ️ Platforms can be on either the Novated or Direct liquidity model to initiate on-demand crypto buys and sells via ACH.
Request an On-demand payment
POST /payments/rfqFollow the same requirements as GET /liquidity/rfq. Sells are ACH or RTP credits and Buys are ACH debits.
The quote duration will default to your platform setting, as configured with Client Services. We recommend a one minute quote with this endpoint to accommodate required balance and authorization checks for the ACH payment. However, you may pass in the optional field quote_expiry with RFQ to override your default.
Request parameters
| Parameter | Description | Required/Optional |
|---|---|---|
side | The side of the quote buy or sell. | Required |
underlying_currency | The underlying asset for the quote. | Required |
quoted_currency | The quoted asset for the quote. | Required |
quantity | The amount of the underlying currency. Either quantity or total must be provided. | Optional |
total | Only for Buy: The desired amount of the quoted_currency for the quote. Either quantity or total must be provided. | Optional |
participant_code | The participant that is requesting to buy/sell. Can be the platform's code or the customer's. | Optional |
account_label | The account label associated with the account. | Optional |
Use onlyquantityfor SellWhen performing a quote to sell crypto, use
quantityfield and the total amount not rounded with all the decimals. This will avoidnot enough crediterrors when executing the RFQ.
Response parameters
| Parameter | Description | Type |
|---|---|---|
| request_id | The identifier of the RFQ | string |
| participant_code | The identifier of the participant making the quote request | string |
| quoted_currency | The asset code for the quoted currency, e.g.USD | string |
| side | The participant side of the quote -buy or sell | string |
| quantity | The amount of the quoted currency | string |
| price | The cost per unit of underlying currency | string |
| quote_id | The identifier for the quote Note: this is required to execute the quote | string |
| expire_ts | Timestamp when the quote will expire | timestamp |
| account_group | The group that the account is a part of | string |
| account_label | The account label associated with the account | string |
| obo_participant | on behalf of participant is the details of the participant benefiting the trade if not the submitter | object |
| network_fee_notional | fee notional in the currency quoted on the RFQ | string |
| network_fee_quantity | fee quantity in the underlying asset | string |
| total_notional | The calculation: ( price * quantity ) + ( network_fee_notional + network_fee_quantity ) | string |
| underlying | The asset code for the underlying currency, e.g. BTC | string |
Execute quote
POST /payments/executePrerequisite
- An external account must already be created via POST /payments/external_accounts.
- A quote must be retained from POST /payments/rfq.
| Parameter | Description | Type |
|---|---|---|
participant_code | The code of the participant who wants to create a new ACH transaction, required. | string |
external_account_id | The unique identifier that zerohash generates to identify the external account. Received from POST /payments/external_accounts, required. | string |
quote_id | The unique identifier assigned to the quote that was executed with POST /payments/rfq, required. | string |
description | Descriptor that travels with the ACH transaction and is intended to show on the participant bank statement. e.g., “COMPANY1”, please use something that helps the customer identify the transaction. Customer statements will show: [Zero Hash] [ZH contact number] [Description]. Note: It is ultimately up to the receiving bank to determine what is shown on the bank statement. Some may ultimately not show the description 10 character limit. | string |
ach_signed_agreement | The time at which the participant agreed to the ACH disclosures. | timestamp |
Sample response
{
"message": {
"request_id": "14f8ebb8-7530-4aa4-bef9-9d73d56313f3",
"quote": {
"request_id": "ce819fe8-b1d7-43bb-961c-e09ede0988d3",
"participant_code": "CUST01",
"quoted_currency": "USD",
"side": "BUY",
"quantity": "1",
"price": "11430.90",
"quote_id": "5cd07738b861c31e3bd61467BTC1Buy1568311644602",
"expire_ts": 1568311649602,
"account_group": "GRP001",
"account_label": "sub_account_test",
"obo_participant": {
"participant_code": "20XRLH",
"account_group": "WRD1K0",
"account_label": "general"
},
"network_fee_notional": "1",
"network_fee_quantity": "1",
"total_notional": "2.00",
"underlying": "BTC",
"asset_cost_notional": "2.00"
},
"trade_id": "ba97133e-ab15-4c86-86c1-86671b8420bc",
"status": "Completed",
"ach_details": {
"inbound_reference_id": "string"
},
"payment_amount": "string",
"external_account_id": "0f68333e-2114-469d-b505-c850d776e063",
"quote_id": "ba97133e-ab15-4c86-86c1-86671b8420bc",
"description": "PLATFORM",
"ach_signed_timestamp": 1561996924964
}
}Check payment status
GET /payments/statusShares the current status of a debit or credit payment. Platforms should use this endpoint to understand funds availability and make their own decisions on how/when to make crypto available to end customers.
Understanding payment statuses follows the existing GET /payments/status flow. Sample response:
{
"message": [
{
"transaction_id": "0f68333e-2114-469d-b505-c850d776e061",
"participant_code": "ALI123",
"amount": "12.01",
"status": "posted",
"transfer_type": "credit",
"bank_transfer_id": "0f68333e-2114-469d-b505-c850d776e061",
"trade_id": "0f68333e-2114-469d-b505-c850d776e061",
"trade_status": "terminated",
"velocity_status": "pending",
"velocity_failed_rule": "participant_transactions(in_flight_transfers_by_participant)",
"created_at": "1975-08-19T23:15:30.000Z",
"updated_at": "1975-08-19T23:15:30.000Z"
}
],
"page": 1,
"total_pages": 1,
"page_size": 200,
"count": 10
}On-demand payment statuses
| Status | Description | Final? |
|---|---|---|
submitted | Transaction request has been received | - |
pending_trade | Transaction request has been approved, but the associated trade has not terminated. | - |
pending | The associated trade has terminated and the fiat payment has been initiated | - |
posted | Transaction has settled and cleared any applicable hold days. Transaction will be reconciled with internal ledger. | - |
settled | Transaction is complete and funds have been credited to platform float | ✔️ |
canceled | ACH On-demand transactions can not be canceled | ✔️ |
failed | Transaction request failed, no payment was initiated | ✔️ |
rejected | Transaction request was rejected due to velocity checks, no payment was initiated | ✔️ |
returned | Transaction request was initially approved but the ACH has returned unsuccessfully, before the transaction reached a 'Settled' state | ✔️ |
returned_settled | Transaction request was initially approved but the ACH has returned unsuccessfully, after the transaction reached a 'Settled' state | ✔️ |
Final statesStates marked are all possible end-states for a transaction, though a transaction can move from
settled→returned_settled
Pre-funded
Pre-funded Integration Guide
After you Link a bank account, you can request fiat deposits and withdrawals. Fiat deposits debit the end customer bank account and credit their zerohash USD account. Fiat withdrawals debit the end customer zerohash USD account and credit their bank account.
Platforms must be on the Direct or Default liquidity model to use fiat deposits and withdrawals.
Request a fiat deposit
POST /paymentsSample request
{
'message': {
'participant_code': 'ABC123',
'external_account_id': '0f68333e-2114-469d-b505-c850d776e063',
'currency': 'USD',
'amount': '150.00',
'description': 'COMPANY0',
'transfer_type': 'debit'
}
}Sample response
{
'message': {
'request_id': '0f65678e-2114-469d-b505-c850d776e078',
'participant_code': 'ABC123',
'external_account_id': '0f68333e-2114-469d-b505-c850d776e063',
'currency': 'USD',
'amount': '150.00',
'description': 'COMPANY0',
'transfer_type': 'debit',
'transaction_id': '0f34533e-2114-469d-b505-c850d776e061',
'status': 'submitted',
'created_at': 1561996924964
}
}Buy crypto with a funded USD account
Pre-funded accounts leverage zerohash’s liquidity endpoints (GET /liquidity/rfq and POST /liquidity/execute). You may ignore sending an account_label as zerohash knows to default to the general account, as assigned when you reached POST /payments.
Sell crypto for USD
Selling acquired assets for USD follows the GET /liquidity/rfq and POST /liquidity/execute flows. Please ignore the funding_details and ach_details mentions. These are only applicable for a deprecated zerohash product.
Withdraw USD to bank account
Withdrawing USD to a customer bank account follows the POST /payments flow, where transfer_type = credit.
Check payment status
GET /payments/statusShares the current status of a debit or credit payment. Platforms should use this endpoint to understand funds availability and make their own decisions on how/when to make funds available to end customers.
Understanding payment statuses follows the existing GET /payments/status flow.
Request parameters
| Status | Description |
|---|---|
submitted | Request received and validated |
pending_trade | Trade associated with the trade id is not terminated |
pending | Balance and authorization confirmed with Plaid and transaction initialized |
posted | Withdrawn from end customer account |
settled | Hold period is over and funds have moved to the bank |
cancelled | Transaction was proactively cancelled |
failed | Transaction failed during the process |
returned | Transaction was returned prior to settlement (reason listed in the reason_code and reason_description of the webhook - most often “insufficient funds”) |
returned_settled | Transaction was returned after settlement (reason listed in the reason_code and reason_description of the webhook) |
rejected | Transaction request blocked for risk mitigation or contractual reasons |
Sample response
{
"message": [
{
"transaction_id": "0f68333e-2114-469d-b505-c850d776e061",
"participant_code": "ALI123",
"amount": "12.01",
"status": "posted",
"transfer_type": "credit",
"bank_transfer_id": "0f68333e-2114-469d-b505-c850d776e061",
"trade_id": "0f68333e-2114-469d-b505-c850d776e061",
"trade_status": "terminated",
"velocity_status": "pending",
"velocity_failed_rule": "participant_transactions(in_flight_transfers_by_participant)",
"created_at": "1975-08-19T23:15:30.000Z",
"updated_at": "1975-08-19T23:15:30.000Z"
}
],
"page": 1,
"total_pages": 1,
"page_size": 200,
"count": 10
}Pre-funded payment statuses
| Status | Description | Final? |
|---|---|---|
submitted | Transaction request has been received | - |
pending | Transaction request has been approved and payment has been initiated | - |
posted | Transaction has settled and cleared any applicable hold days. Transaction will be reconciled with internal ledger. | - |
settled | Transaction is complete and funds have been made available to participant | ✔️ |
canceled | Transaction request has been canceled, no payment was initiated | ✔️ |
failed | Transaction request failed, no payment was initiated | ✔️ |
rejected | Transaction request was rejected due to velocity checks, no payment was initiated | ✔️ |
returned | Transaction request was initially approved but the ACH has returned unsuccessfully, before the transaction reached a 'Settled' state | ✔️ |
returned_settled | Transaction request was initially approved but the ACH has returned unsuccessfully, after the transaction reached a 'Settled' state | ✔️ |
Final statesStates marked are all possible end-states for a transaction, though a transaction can move from
settled→returned_settled
Instant USD
Instant USD Integration Guide
Platforms must use zerohash Powered Plaid Link or Self Service Plaid Link to verify and link end customer bank accounts. Once an external account has been created and linked to a Participant account, Platforms can make ACH deposited funds instantly available to the end customer using zerohash Instant USD.
Request an Instant fiat deposit
POST /fund/depositSample request
{
'message': {
'participant_code': 'ABC123',
'external_account_id': '0f68333e-2114-469d-b505-c850d776e063',
'asset': 'USD',
'amount': '150.00',
'description': 'COMPANY0',
'network': 'ach',
'instant': true
}
}Sample response
{
'message': {
'request_id': '0f65678e-2114-469d-b505-c850d776e078',
'participant_code': 'ABC123',
'external_account_id': '0f68333e-2114-469d-b505-c850d776e063',
'asset': 'USD',
'amount': '150.00',
'description': 'COMPANY0',
'transaction_id': '0f34533e-2114-469d-b505-c850d776e061',
'status': 'submitted',
'created_at': 1561996924964
}
}Available balance
It can take 3-5 days for ACH transactions to settle. In the meantime, zerohash offers end customers an immediate available balance to make trades. In the Instant ACH flow, funds are immediately transferred from the platform float to the participants available balance. The platform can expect to receive a transfer webhook with the following information:
{
"account_group": "F12JE6",
"account_label": "general",
"account_type": "available",
"asset": "USD",
"balance": "5",
"movements": [
{
"account_id": "efeb40fe-aaf1-4132-8fbc-036e7612f7db",
"change": "5",
"movement_id": "b96daa8d-960a-4c50-acc1-af9c1c9c178b",
"movement_timestamp": 1746112183373,
"movement_type": "transfer",
"transfer_request_id": "339910",
"transfer_type": "trade_allocation"
}
],
"participant_code": "45JM2Q",
"run_id": "4150531",
"run_type": "transfer",
"timestamp": 1746112183373
}Buy crypto with USD Instantly
End customers are immediately able to purchase crypto using the available balance in their participant account with zerohash’s CLOB liquidity endpoints or with RFQ endpoints (depending on the trading model leveraged).
Crypto buy orders will settle immediately even if the ACH deposit has not settled. Platforms will receive web hooks for both events that look like this:
ACH deposit settlement
zerohash returns a webhook with 'status': 'settled' when an ACH deposit clears.
{
'message': {
'request_id': '0f65678e-2114-469d-b505-c850d776e078',
'participant_code': 'ABC123',
'external_account_id': '0f68333e-2114-469d-b505-c850d776e063',
'asset': 'USD',
'amount': '150.00',
'description': 'COMPANY0',
'transfer_type': 'debit',
'transaction_id': '0f34533e-2114-469d-b505-c850d776e061',
'status': 'settled',
'created_at': 1561996924964
}
}Crypto Buy Settlement
zerohash returns a webhook with 'trade_state': 'terminated' when an buy order settles. Find more information on trade webhooks here.
{
"client_trade_id": "b3fbcf52-95f9-4624-9309-4f1306bdb176",
"contract_size": "1",
"market_identifier_code": "SCXM",
"origin": "liquidity",
"parties": [
{
"account_label": "general",
"amount": "10",
"asset": "USD",
"participant_code": "PART1",
"settling": true,
"side": "sell"
},
{
"account_label": "inventory",
"amount": "0.0057236",
"asset": "ETH",
"participant_code": "00SCXM",
"settling": true,
"side": "buy"
}
],
"parties_anonymous": false,
"physical_delivery": true,
"platform_code": "00SCXM",
"product_type": "spot",
"quoted_currency": "USD",
"symbol": "ETH/USD",
"timestamp": 1745554838289,
"total_notional": "10.00",
"trade_id": "26d68f16-f9d2-4ab3-847e-bccf6f03a35e",
"trade_price": "1747.1521420085261025",
"trade_quantity": "0.0057236",
"trade_reporter": "00SCXM",
"trade_reporter_code": "00SCXM",
"trade_state": "terminated",
"trade_type": "regular",
"transaction_timestamp": 1745554838000,
"underlying": "ETH"
}{
'message': {
'request_id': '0f65678e-2114-469d-b505-c850d776e078',
'participant_code': 'ABC123',
'external_account_id': '0f68333e-2114-469d-b505-c850d776e063',
'asset': 'USD',
'amount': '150.00',
'description': 'COMPANY0',
'transaction_id': '0f34533e-2114-469d-b505-c850d776e061',
'status': 'submitted',
'created_at': 1561996924964
}
}Instant USD payment statuses
| Status | Description | Final? |
|---|---|---|
submitted | Transaction request has been received | - |
pending_trade | Transaction request has been approved, but the associated trade has not terminated. | - |
pending | The associated trade has terminated and the fiat payment has been initiated | - |
posted | Transaction has settled and cleared any applicable hold days. Transaction will be reconciled with internal ledger. | - |
settled | Transaction is complete and funds have been credited to platform float | ✔️ |
canceled | ACH On-demand transactions can not be canceled | ✔️ |
failed | Transaction request failed, no payment was initiated | ✔️ |
rejected | Transaction request was rejected due to velocity checks, no payment was initiated | ✔️ |
returned | Transaction request was initially approved but the ACH has returned unsuccessfully, before the transaction reached a 'Settled' state | ✔️ |
returned_settled | Transaction request was initially approved but the ACH has returned unsuccessfully, after the transaction reached a 'Settled' state | ✔️ |
Final statesStates marked are all possible end-states for a transaction, though a transaction can move from
settled→returned_settled
Account Types
Loss Reserve
For ACH, platforms fund a loss reserve balance to absorb any losses tied to ACH returns, which can occur up to 60 days after a debit has settled. It applies across all ACH debit flows.
RTP payments do not require a loss reserve, as RTP transactions are final once settled.
Reserve sizing is set by our financial risk team based on your expected volumes and use case.
Float Account
Float balances are funded to ensure funds can be made available to customers before the underlying payment settles. Whether it is required depends on the model:
- On-demand and Instant USD: a float balance funds a user's trade or balance before their ACH deposit settles. A typical starting point for funding a float is 3–4 days of transaction volume, with additional headroom for volume spikes.
- RTP: because RTP operates 24/7/365, an RTP float balance ensures that credits can be completed outside of banking hours. Sizing follows the same approach as On-demand: a buffer covering several days of volume, adjusted for expected spikes.
No float balance is required for Pre-funded, since in this model funds are not made available to a user until the ACH transfer settles, and a brief holding period has lapsed.
Webhook notifications
Webhooks let you know the status of a payment and if funds are available to the end customer or platform. The payload is a JSON object containing the following fields:
| Parameter | Description | Type |
|---|---|---|
participant_code | The zerohash identifier for the customer requesting a Fiat 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 zerohash 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 (for ACH), 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 |
For more information on the payments webhooks, check out our documentation here.
ACH returns
ACH returns are available in GET /payments/status under the status returned. While webhooks share the reason for returns, currently the GET call does not include this information.
Platforms must maintain a funded loss reserve and float account. When an ACH deposit fails, the participant account’s balance and buying power will be deducted accordingly. If buying power has already been used to initiate a crypto buy order, the pending order will be canceled. Losses will be recovered from the Platform.
- If an ACH fails before settlement and buying power has been used to purchase an asset, zerohash will seize it from the participant account, liquidate it, and recover any losses from the Platform’s float account.
- If an ACH is returned after settlement and funds cannot be recovered from the participant account, zerohash will recover losses from the Platform’s loss reserve account.
Unauthorized returns
An unauthorized return means that the bank account owner has flagged an issue with the transaction. When these returns are received, zerohash immediately locks the end customer account in an effort to mitigate further risk. The zerohash compliance team must evaluate risk before moving the participant back to approved, or along to disabled (more on participant statuses).
Participant status webhooks alert platforms to movements from approved to locked. In the case of unauthorized returns, those results share a reason of ach.
Updated 9 days ago