Crypto buy/sell via ACH API Workflow

End customers can buy a specified amount of crypto with USD directly from their bank account. End customers can sell a specific amount of crypto for USD, and withdraw directly to their bank account.

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 must be on the Novated liquidity model to use crypto buys and sells via ACH.

Request an on-demand payment

POST /payments/rfq

Follow the same requirements as GET /liquidity/rfq. Sells are ACH 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

sideThe side of the quote buy or sell.Required
underlyingThe underlying asset for the quote.Required
quoted_currencyThe quoted asset for the quote.Required
quantityThe amount of the underlying currency. Either quantity or total must be provided.Optional
totalThe desired amount of the quoted_currency for the quote. Either quantity or total must be provided.Optional
participant_codeThe participant that is requesting to buy/sell. Can be the platform's code or the customer's.Optional
account_labelThe account label associated with the account.Optional

Response parameters

request_idThe identifier of the RFQstring
participant_codeThe identifier of the participant making the quote requeststring
quoted_currencyThe asset code for the quoted currency, e.g.USDstring
sideThe participant side of the quote -buy or sellstring
quantityThe amount of the quoted currencystring
priceThe cost per unit of underlying currencystring
quote_idThe identifier for the quote
Note: this is required to execute the quote
expire_tsTimestamp when the quote will expiretimestamp
account_groupThe group that the account is a part ofstring
account_labelThe account label associated with the accountstring
obo_participanton behalf of participant is the details of the participant benefiting the trade if not the submitterobject
network_fee_notionalfee notional in the currency quoted on the RFQstring
network_fee_quantityfee quantity in the underlying assetstring
total_notionalThe calculation:
( price * quantity ) + ( network_fee_notional + network_fee_quantity )
underlyingThe asset code for the underlying currency, e.g.BTCstring

Execute quote

POST /payments/execute


participant_codeThe code of the participant who wants to create a new ACH transaction, required.string
external_account_idThe unique identifier that Zero Hash generates to identify the external account. Received from POST /payments/external_accounts, required.string
quote_idThe unique identifier assigned to the quote that was executed with POST /payments/rfq, required.string
descriptionDescriptor 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.
ach_signed_agreementThe 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/status

Shares 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.

Request parameters

submittedRequest received and validated
pending_tradeTrade associated with the trade id is not terminated
pendingBalance and authorization confirmed with Plaid and transaction initialized
postedWithdrawn from end customer account
settledHold period is over and funds have moved to the bank
cancelledTransaction was proactively cancelled
failedTransaction failed during the process
returnedTransaction was returned prior to settlement (reason listed in the reason_code and reason_description of the webhook - most often “insufficient funds”)
returned_settledTransaction was returned after settlement (reason listed in the reason_code and reason_description of the webhook)
rejectedTransaction 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

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:

participant_codeThe Zero Hash identifier for the customer requesting an ACH transaction.string
typeIndicates if the payment is a credit or a debit for the end customerstring
transaction_idThe unique identifier generated by Zero Hash for the transaction.string
payment_statusThe current status of the payment. See payment statuses for more.string
reason_codeThe NACHA failure reason code, if the payment_status is returned.string
reason_descriptionThe description matching the reason_code, if the payment_status is returned.string
expected_settlement_dateFor 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.

Return funding

When a return comes in, Zero Hash will first check the end customer account to attempt to recover funds:

  1. Full funds available in end customer account: Zero Hash funds the return with end customer balance.
  2. Partial funds available in end customer account: Zero Hash funds as much of the return as possible with end customer balance, and pulls the remaining balance from platform loss reserves.
  3. No funds available in end customer account: Zero Hash funds the return with platform loss reserves.

Unauthorized returns

An unauthorized return means that the bank account owner has flagged an issue with the transaction. When these returns are received, Zero Hash immediately locks the end customer account in an effort to mitigate further risk. The Zero Hash 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.