Off Ramp Integration Guide

Seamless conversion between crypto and fiat - end to end technical integration guide

Introduction

zerohash's Off Ramp product allows any business to quickly launch an experience that allows their end customers to convert crypto to fiat

Definitions

TermDefinition
PlatformThe company that's in contract with zerohash and directly interacts with zerohash's APIs or SDKs.
End CustomerThe end retail customer that has a direct relationship with the Platform and zerohash.

High Level Flow

  1. Submit End Customer
  2. Query End Customer
  3. Create Deposit Address
  4. Initiate deposit
  5. Get Quote
  6. Execute Quote
  7. Query Deposit and Trade
  8. Network Fee Procedures
  9. Email Receipt Requirements
  10. Settlement

Initial Setup

General

The Platform will need to onboard its business to the zerohash platform. They then need to add additional users, create API keys, and whitelist IP addresses. See instructions here.

Webhooks

You'll want to listen to our Account Balance webhook events - this will notify you of a crypto deposit to a zerohash account, among other balance updating events.

To get configured, reach out to your zerohash rep who can quickly configure your webhook callback URL

1. Submit End Customer

Once API keys are created and approved, the Platform can begin to integrate to the API. For the following examples, we’ll assume the Platform's platform code is PLAT01.

The Platform submits the End Customer via POST /participants/customers/new:

{
    "first_name": "John",
    "last_name": "Smith",
    "email": "[email protected]",
    "phone_number": "+12345834789",
    "date_of_birth": "1985-09-02",
    "ip_address": "999.168.252.199",
    "address_one": "1 Main St.",
    "address_two": "Suite 1000",
    "city": "Chicago",
    "zip": "12345",
    "jurisdiction_code": "US-IL",
    "partial": true,
    "sanction_screening": "pass",
    "sanction_screening_timestamp": 1603378501282,
    "signed_agreements": [{
        "type": "user_agreement",
        "region": "us",
        "signed_timestamp": 1603378501286
    }]
}

Note: The End Customer will be agreeing to the zerohash terms and conditions. The platform indicates to zerohash that they accepted the terms by sending a value in the signed_agreement object as shown above.

  • type: the type of user agreement being signed. For On and Off Ramps, the End Customer is signing the standard user agreement, so a value of user_agreement is expected.
  • signed_timestamp: is the timestamp that the End Customer accepted the terms and conditions.
  • region: represents the zerohash entity that the End Customer is entering into a contract with. The only acceptable values right now are US and EU

POST /participants/customers/new response:

 {
     "first_name": "John",
     "last_name": "Smith",
     "email": "[email protected]",
     "address_one": "1 Main St.",
     "address_two": "Suite 1000",
     "country": "United States",
     "state": "IL",
     "jurisdiction_code": "US-IL",
     "city": "Chicago",
     "zip": "12345",
     "date_of_birth": "1985-09-02",
     "id_number_type": "unknown",
     "id_number": "",
     "non_us_other_type": "",
     "id_issuing_authority": "",
     "signed_timestamp": -62135596800000,
     "risk_rating": "",
     "metadata": {},
     "platform_code": "PLAT01",
     "participant_code": "CUST01",
     "tax_id": "",
     "citizenship": "",
     "citizenship_code": "",
     "kyc": "unknown",
     "kyc_timestamp": null,
     "onboarded_location": "US-IL",
     "sanction_screening": "pass",
     "sanction_screening_timestamp": null,
     "liveness_check": "unknown",
     "phone_number": "+12345834789",
     "employment_status": "unknown",
     "industry": "unknown",
     "source_of_funds": "unknown",
     "signed_agreements": [{
         "region": "us",
         "signed_timestamp": 1603378501,
         "type": "user_agreement"
     }]
 }

Note: The response contains a participant_code for the End Customer. This uniquely identifies the End Customer indefinitely and should be used in subsequent API calls where applicable. We’ll use CUST01 as the participant_code throughout the examples.

2. Query End Customer

The Platform can query the End Customer via the GET /participants/{email} endpoint or the Platform can query all End Customers via the GET /participants endpoint

3. Create Deposit Address

Begin by creating a crypto deposit address for your customer via the POST /deposits/digital_asset_addresses endpoint.

Example request:

 {
     "participant_code": "CUST01",
     "asset": "SOL"
 }

Example response (see address field for where crypto should be deposited to):

{
  "message": {
    "address": "7EcDhSYGxXyscszYEp35KHN8vvw3svAuLKTzXwCFLtV",
    "participant_code": "CUST01",
    "platform_code": "PLAT01",
    "asset": "SOL",
    "account_label": "general",
    "created_at": 1590663417000
  }
}

4. Initiate deposit

The Customer will initiate an on-chain deposit to the above address. Once zerohash has received it, we will:

  • Update the Customer balance on our ledger
  • Send an Account Balance webhook to the Platform. Example payload:
{
    "account_group": "PLAT01",
    "account_label": "general",
    "account_type": "available",
    "asset": "SOL",
    "balance": "1",
    "movements": [
        {
            "account_id": "363d0c5f-1163-47c9-8de2-97cfbecdd1e6",
            "change": "1.00",
            "deposit_reference_id": "5VfydnLu4XQL8QMcXUZW6ZFAsCRHFyj4WT5ftvHc7TCXqvL1F9YWnwvQdBUcSVR9pRZaZG5w9j4NrQXaJnpFVKZ8",
            "movement_id": "bcc65fa1-1a1d-4a77-9f0b-cad3968b50db",
            "movement_timestamp": 1590663417999,
            "movement_type": "deposit"
        }
    ],
    "participant_code": "CUST01",
    "run_id": "4149614",
    "run_type": "deposit",
    "timestamp": 1743593652
}

5. Get Quote

Now, you can off ramp the deposited crypto into fiat by using our rfq endpoints.

There are 2 ways to monetize this transaction:

  • Spreads: zerohash can pre-configure a percentage amount inflates the sale price, allowing zerohash and the Platform to collectively monetize the transaction
  • Fees: Additionally, you can choose to charge explicit, non-percentage-based fees. See fees object below.

The first step is to get a quote via the POST /liquidity/rfq endpoint:

{
  "participant_code": "CUST01",
  "side": "sell",
  "underlying": "SOL",
  "quoted_currency": "USD",
  "quantity": "1",
  "fees": [
    { "amount": "2.50", "name": "processing-1" }
  ]
}

Response:

{
  "message": {
    "request_id": "14f8ebb8-7530-4aa4-bef9-9d73d56313f3",
    "participant_code": "CUST01",
    "quoted_currency": "USD",
    "underlying": "SOL",
    "side": "sell",
    "quantity": "1",
    "price": "500.00",
    "quote_id": "32ad471b-824e-4f04-94ff-45c4439b4fe9",
    "expire_ts": 1751830000000,
    "account_group": "PLAT01",
    "account_label": "general",
    "obo_participant": {
      "participant_code": "CUST01",
      "account_group": "PLAT01",
      "account_label": "general"
    },
    "asset_cost_notional": "500.00",
    "spread_notional": "2.50",
    "spread_bps": "50",
    "commission_notional": null,
    "commission_bps": null,
    "total_notional": "495.00",
    "fees": [
      { "name": "processing-1", "amount": "2.50" }
    ]
  }
}

Note:

  • fee_inclusive = true can be enacted to ensure that the End Customer spends a maximum of total and the fees will be deducted from there
  • Typically for off ramps the quote length is 30 seconds, although this is configurable

6. Execute Quote

The platform will then execute a POST /liquidity/execute call using the quote_id from the response above:

{
  "quote_id": "32ad471b-824e-4f04-94ff-45c4439b4fe9"
}

Response:

{
  "message": {
    "request_id": "14f8ebb8-7530-4aa4-bef9-9d73d56313f3",
    "quote": {
      "request_id": "14f8ebb8-7530-4aa4-bef9-9d73d56313f3",
      "participant_code": "CUST01",
      "quoted_currency": "USD",
      "underlying": "SOL",
      "side": "sell",
      "quantity": "1",
      "price": "500.00",
      "quote_id": "32ad471b-824e-4f04-94ff-45c4439b4fe9",
      "expire_ts": 1751830000000,
      "account_group": "PLAT01",
      "account_label": "general",
      "obo_participant": {
        "participant_code": "CUST01",
        "account_group": "PLAT01",
        "account_label": "general"
      },
      "asset_cost_notional": "500.00",
      "spread_notional": "2.50",
      "spread_bps": "50",
      "commission_notional": null,
      "commission_bps": null,
      "total_notional": "495.00",
      "fees": [
        { "name": "processing-1", "amount": "2.50" }
      ]
    },
    "trade_id": "ba97133e-ab15-4c86-86c1-86671b8420bc",
    "trade_ids_list": [
      "ba97133e-ab15-4c86-86c1-86671b8420bc"
    ],
    "status": "Completed",
    "ach_details": null
  }
}

After the trade settles, the end result is a credit to the Platform's Float Account:

  • participant_code: 00SCXM
  • account_group: [platform participant_code]
  • account_label: general
  • asset: USD
  • account_type: available

7. Query Deposit and Trade

Platforms can retroactively query deposits and trades via our REST endpoints. See examples below:

GET /deposits or GET /deposits/{deposit_id} example response:

{
  "message": [
    {
      "settle_timestamp": 1751830000000,
      "account_id": "80289ce5-072b-4739-929d-dcc5ccc542f3",
      "movement_id": "21bdbb11-712f-4cb7-a178-4f277c80cde0",
      "participant_code": "CUST01",
      "account_group": "CUST01_default",
      "account_label": "general",
      "asset": "SOL",
      "amount": "1.0000",
      "reference_id": "5VfydnLu4XQL8QMcXUZW6ZFAsCRHFyj4WT5ftvHc7TCXqvL1F9YWnwvQdBUcSVR9pRZaZG5w9j4NrQXaJnpFVKZ8",
      "source": null,
      "received_address": "7EcDhSYGxXyscszYEp35KHN8vvw3svAuLKTzXwCFLtV",
      "run_id": "12613",
      "state_reason": null,
      "travel_rule_status": "not_required"
    }
  ],
  "page": 1,
  "page_size": 15,
  "total_pages": 1,
  "count": 1
}

GET /trades or GET /trades/{trade_id} example response:

{
  "message": [
    {
      "batch_trade_id": null,
      "trade_id": "ba97133e-ab15-4c86-86c1-86671b8420bc",
      "client_trade_id": "32ad471b-824e-4f04-94ff-45c4439b4fe9",
      "trade_reporter": "[email protected]",
      "trade_reporter_code": "PLAT01",
      "reporting_party": null,
      "settlement_schedule": null,
      "platform_code": "PLAT01",
      "market_identifier_code": null,
      "symbol": "SOL/USD",
      "product_type": "spot",
      "trade_type": "regular",
      "trade_price": "500.00",
      "trade_quantity": "1",
      "trade_state": "terminated",
      "physical_delivery": true,
      "transaction_timestamp": 1751830050000,
      "accepted_timestamp": 1751830050500,
      "defaulted_timestamp": null,
      "settled_timestamp": 1751830051200,
      "comment": null,
      "last_update": 1751830051200,
      "parties_anonymous": false,
      "settlement_price_index_id": null,
      "contract_size": 1,
      "underlying": "SOL",
      "quoted_currency": "USD",
      "settlement_timestamp": null,
      "expiry_timestamp": null,
      "bank_fee": null,
      "spread_notional": "2.50",
      "spread_bps": "50",
      "issuer_fee_rate": null,
      "issuer_fee_amount": null,
      "issuer_fee_payor_type": null,
      "origin": "rest_api",
      "parties": [
        {
          "participant_code": "CUST01",
          "side": "sell",
          "asset": "SOL",
          "amount": "1",
          "liquidity_indicator": null,
          "client_order_id": null,
          "order_id": null,
          "obligations_outstanding_timestamp": null,
          "current_obligations_met_timestamp": 1751830051200,
          "settlement_state": "settled",
          "execution_id": null,
          "settling": true,
          "account_label": "general",
          "collateral_percentage": null,
          "account_profile": null,
          "trader": null,
          "zrn": null,
          "manual_order_indicator": null
        },
        {
          "participant_code": "00SCXM",
          "side": "buy",
          "asset": "USD",
          "amount": "500.00",
          "liquidity_indicator": null,
          "client_order_id": null,
          "order_id": null,
          "obligations_outstanding_timestamp": null,
          "current_obligations_met_timestamp": 1751830051200,
          "settlement_state": "settled",
          "execution_id": null,
          "settling": true,
          "account_label": "inventory",
          "collateral_percentage": null,
          "account_profile": null,
          "trader": null,
          "zrn": null,
          "manual_order_indicator": null
        }
      ],
      "session_id": "",
      "payment_processor": null,
      "fees": [
        { "name": "processing-1", "amount": "2.50" }
      ],
      "network_fee_notional": null,
      "network_fee_quantity": null,
      "total_notional": "495.00",
      "asset_cost_notional": "500.00"
    }
  ],
  "page": 1,
  "page_size": 15,
  "total_pages": 1,
  "count": 1
}

8. Network Fee Procedures

The lone network fee consideration with this flow has to do with the creation of the deposit address. Typically, the Platform will incur this fee.

9. Email Receipt Requirements

zerohash requires that the End Customer receives an email receipt for each transaction. We also insist that the email contain:

  1. Certain fields and values, which can be obtained using our API
  2. Adequate support contact information

Fields

Here are the required field names and their associated API fields:

Email Receipt Field NameDescriptionExampleAPI Location
Order NumberUnique order identifier9a738372-0855-4b25-8c65-5de0aa858b8btrade_id from GET /trades
Order TypeThe type of orderOff Ramp TransactionN/a, can hard-code to "Off Ramp Transaction Transmission"
Transmission AmountThe transmission amount for the transaction, in quoted currency terms$10total from POST /liquidity/rfq or POST /convert_withdraw/execute response
Amount Received by End CustomerThe amount of the underlying crypto received by the End Customer10 USDCquantity from POST /liquidity/rfq or POST /liquidity/execute response
Fees

Any added fee values included in the transaction. If there are no fees, then this needs to be expressly stated.

This includes processing fees (ie, a fee assessed by the Platform to the End Customer) or blockchain network fees.

"$0", "1.99", "$0 Fees", or "No Fees"fees from POST /liquidity/rfq and POST /liquidity/execute endpoints
Date/TimeDate and timestamp of the transaction transmission (meaning the time that the fiat was converted and settled into crypto within the zerohash system)2024-10-11 18:38:00settled_timestamp from GET /trades
Account IDzerohash's unique account identifier for the End Customer ( the “participant code” within zerohash).CUST01participant_code from POST /liquidity/rfq and POST /liquidity/execute endpoints

10. Settlement

zerohash will, one time per day, send a fiat settlement wire to the Platform where the amount represents the sum of all converted deposits from the prior trading session. Here is the settlement schedule:

SessionStartEndExpected Settlement Time*
MondayMonday 9:00a ESTTuesday 8:59:59a ESTTuesday EOD
TuesdayTuesday 9:00a ESTWednesday 8:59:59a ESTWednesday EOD
WednesdayWednesday 9:00a ESTThursday 8:59:59a ESTThursday EOD
ThursdayThursday 9:00a ESTFriday 8:59:59a ESTFriday EOD
FridayFriday 9:00a ESTMonday 8:59:59a ESTMonday EOD

During US holidays, Platforms should expect their settlements to arrive by EOD on the next business day. For example, for the August 30th 2024 session, the settlement will arrive by Tuesday EOD (because Monday was Labor Day)

Manual settlement initiation

Platforms can manually initiate settlements outside of the settlement schedule using POST /withdrawals/requests endpoint. Example request:

{
  "asset": "USD",
  "participant_code": "PLAT01",
  "amount": "50000",
  "account": {
    "type": "INTERNATIONAL_FIAT",
    "name": "Primary (1234)",
    "account_number": "1231234",
    "swift_code": "123456789",
    "beneficiary_name": "John Doe",
    "bank_name": "BMO Harris",
    "intermediary_bank_account_number": "1231234",
    "intermediary_bank_name": "Intermediary Bank Name"
    },
  "account_label": "general"
}

Tracking the settlement amount

The recommended way to track the impending settlement amount is to query the GET /accounts/{account\_id} endpoint. In order to retrieve the correct account_id to use in that call, use the following query parameters when hitting the GET /accounts endpoint:

  • participant_code: [your platform code]
  • account_group: [your platform code]
  • account_label: general
  • account_type: available
  • asset: [fiat currency code, typically "USD"]

Use the account_id in the response to query associated accounts. Example response:

{
    "message": [
        {
            "asset": "USD"
            "account_owner": "PLAT01",
            "account_type": "available",
            "account_group": "PLAT01",
            "account_label": "general",
            "balance": "25000",
            "account_id": "af063ca6-836f-4677-8ebf-edbe1d049938",
            "last_update": 1727214271011
        }
    ]
}

Retroactively querying settlements in the past

Platform settlements are technically a withdrawal. In order to query settlements from the past, use the GET /withdrawals/requests endpoint. The response will show all withdrawals made from the Platform's account, or one of your Customer's (Customer withdrawals are extremely rare and reserved for edge case Account Funding failures only).

In order to filter for withdrawals made from the Platform account only, query GET /withdrawals/requests using the participant_code query parameter, where the value is the Platform's platform code:

Example GET /withdrawals/requests?participant_code=PLAT01 response:

{
    "message": [
        {
            "id": "152b8276-0585-45ec-bf62-85e134b3ff43",
            "withdrawal_account_id": 346030,
            "participant_code": "PLAT01",
            "account_group": "PLAT01",
            "account_label": null,
            "requestor_participant_code": "PLAT01",
            "asset": "USDC.SOL",
            "requested_amount": "25000",
            "settled_amount": "25000",
            "gas_price": null,
            "status": "SETTLED",
            "on_chain_status": "",
            "client_withdrawal_request_id": null,
            "requested_timestamp": 1727921516403,
            "transaction_id": "HND48J83HGR55LLP",
            "input_data": null,
            "fee_amount": "",
            "quoted_fee_amount": null,
            "quoted_fee_notional": null,
            "trade_id": null,
            "quoted_fee_asset": null,
            "withdrawal_fee": "",
            "parent_link_id": null,
            "parent_link_id_source": null
        }
    ]
}





Did this page help you?