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
| Term | Definition |
|---|---|
| Platform | The company that's in contract with zerohash and directly interacts with zerohash's APIs or SDKs. |
| End Customer | The end retail customer that has a direct relationship with the Platform and zerohash. |
High Level Flow
- Submit End Customer
- Query End Customer
- Create Deposit Address
- Initiate deposit
- Get Quote
- Execute Quote
- Query Deposit and Trade
- Network Fee Procedures
- Email Receipt Requirements
- 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 ofuser_agreementis 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 areUSandEU
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
feesobject 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 oftotaland 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: 00SCXMaccount_group: [platformparticipant_code]account_label: generalasset: USDaccount_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:
- Certain fields and values, which can be obtained using our API
- Adequate support contact information
Fields
Here are the required field names and their associated API fields:
| Email Receipt Field Name | Description | Example | API Location |
|---|---|---|---|
| Order Number | Unique order identifier | 9a738372-0855-4b25-8c65-5de0aa858b8b | trade_id from GET /trades |
| Order Type | The type of order | Off Ramp Transaction | N/a, can hard-code to "Off Ramp Transaction Transmission" |
| Transmission Amount | The transmission amount for the transaction, in quoted currency terms | $10 | total from POST /liquidity/rfq or POST /convert_withdraw/execute response |
| Amount Received by End Customer | The amount of the underlying crypto received by the End Customer | 10 USDC | quantity 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/Time | Date 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:00 | settled_timestamp from GET /trades |
| Account ID | zerohash's unique account identifier for the End Customer ( the “participant code” within zerohash). | CUST01 | participant_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:
| Session | Start | End | Expected Settlement Time* |
|---|---|---|---|
| Monday | Monday 9:00a EST | Tuesday 8:59:59a EST | Tuesday EOD |
| Tuesday | Tuesday 9:00a EST | Wednesday 8:59:59a EST | Wednesday EOD |
| Wednesday | Wednesday 9:00a EST | Thursday 8:59:59a EST | Thursday EOD |
| Thursday | Thursday 9:00a EST | Friday 8:59:59a EST | Friday EOD |
| Friday | Friday 9:00a EST | Monday 8:59:59a EST | Monday 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: generalaccount_type: availableasset: [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
}
]
}
zerohash B.V. (EU entity)The following steps should be followed if you're onboarding a Customer to zerohash B.V. (the EU entity):
Base URL changeFor EU API calls, use the following base URL: https://api.zerohash.eu
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
| Term | Definition |
|---|---|
| Platform | The company that's in contract with zerohash and directly interacts with zerohash's APIs or SDKs. |
| End Customer | The end retail customer that has a direct relationship with the Platform and zerohash. |
High Level Flow
- Submit End Customer
- Query End Customer
- Create Deposit Address
- Initiate deposit
- Get Quote
- Execute Quote
- Query Deposit and Trade
- Network Fee Procedures
- Email Receipt Requirements
- 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.
Submit customer
Natural Person
Example request - POST /participants/customers/new
{
"request_id": "c3d5e7f9-1a2b-4c6d-8e0f-3a5b7c9d1e2f",
"first_name": "John",
"last_name": "Smith",
"email": "[email protected]",
"phone_number": "+33612345678",
"date_of_birth": "1985-09-02",
"address_one": "123 Rue de Rivoli",
"city": "Paris",
"postal_code": "75001",
"jurisdiction_code": "FR-75",
"citizenship_code": "FR",
"tax_residence": {
"jurisdiction_code": "FR-75"
},
"tax_id": "1234567890123",
"id_number_type": "passport",
"id_number": "12AB34567",
"id_issuing_date": "2020-12-16",
"id_expiration_date": "2030-12-15",
"employment_status": "full_time",
"industry": "technology",
"source_of_funds": "salary",
"savings_and_investments": "under_10000",
"risk_rating": "low",
"kyc": "pass",
"kyc_timestamp": 1603378501286,
"sanction_screening": "pass",
"sanction_screening_timestamp": 1603378501286,
"idv": "pass",
"liveness_check": "pass",
"signed_timestamp": 1603378501286,
"self_certification_timestamp": 1667504636159,
"signed_agreements": [
{
"type": "user_agreement",
"region": "eu",
"signed_timestamp": 1603378501286
}
]
}Non-natural person
To submit a non-natural person customer, use the POST /participants/entity/new endpoint. Example request:
{
"request_id": "d4e6f8a0-2b3c-4d7e-9f1a-4b6c8d0e2f3a",
"entity_type": "corporation",
"platform_code": "PLAT01",
"entity_name": "Entity Name",
"legal_name": "Legal Name",
"contact_number": "+33140123456",
"date_established": "2015-09-02",
"address_one": "123 Rue de Rivoli",
"city": "Paris",
"postal_code": "75001",
"jurisdiction_code": "FR-75",
"country": "FRA",
"tax_id": "12345678901",
"id_issuing_authority": "FR",
"id_number": "549300TRWGQX5XMHYZ82",
"id_number_type": "legal_entity_identifier",
"chamber_of_commerce_number": "123456789",
"activity_locations": ["FR", "PT"],
"entity_source_of_funds": "crypto_activities",
"expected_annual_volume": "over_500k",
"declaration_no_foreseeable_changes": true,
"reason_for_business_relationship": "Digital asset custody and trading services for institutional clients",
"risk_rating": "low",
"sanction_screening": "pass",
"sanction_screening_timestamp": 1603378501286,
"signed_timestamp": 1603378501286,
"submitter_email": "[email protected]",
"signed_agreements": [
{
"type": "user_agreement",
"region": "eu",
"signed_timestamp": 1603378501286
}
],
"control_persons": [
{
"name": "Jane Smith",
"first_name": "Jane",
"last_name": "Smith",
"email": "[email protected]",
"phone_number": "+33612345678",
"address_one": "123 Rue de Rivoli",
"city": "Paris",
"postal_code": "75001",
"jurisdiction_code": "FR-75",
"date_of_birth": "1985-09-02",
"citizenship_code": "FR",
"tax_id": "1234567890123",
"id_number_type": "passport",
"id_number": "12AB34567",
"id_issuing_authority": "FR",
"sanction_screening": "pass",
"sanction_screening_timestamp": 1603378501286,
"kyc": "pass",
"kyc_timestamp": 1603378501286
}
],
"beneficial_owners": [
{
"name": "Jane Smith",
"first_name": "Jane",
"last_name": "Smith",
"email": "[email protected]",
"phone_number": "+33612345678",
"address_one": "123 Rue de Rivoli",
"city": "Paris",
"postal_code": "75001",
"jurisdiction_code": "FR-75",
"date_of_birth": "1985-09-02",
"citizenship_code": "FR",
"tax_id": "1234567890123",
"id_number_type": "passport",
"id_number": "12AB34567",
"id_issuing_authority": "FR",
"sanction_screening": "pass",
"sanction_screening_timestamp": 1603378501286,
"kyc": "pass",
"kyc_timestamp": 1603378501286
}
],
"board_members": [
{
"first_name": "Jane",
"last_name": "Smith",
"email": "[email protected]",
"address_one": "123 Rue de Rivoli",
"city": "Paris",
"postal_code": "75001",
"jurisdiction_code": "FR-75",
"date_of_birth": "1985-09-02",
"citizenship_code": "FR",
"place_of_birth_country_code": "FR",
"place_of_birth_name": "Paris",
"tax_id": "1234567890123",
"id_number_type": "passport",
"id_number": "12AB34567",
"id_issuing_authority": "FR",
"sanction_screening": "pass",
"sanction_screening_timestamp": 1603378501286
}
]
}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.
You can make your requests idempotent by using the request_id field. If a subsequent call is made with a previously used request_id, then the request would fail with an error code 400, error message “requestID already used with different participant data”.
If you fail to indicate that the End Customer has agreed to the user agreement, in the signed_agreements object shown above, future API calls will fail.
There may be situations where the Platform is restricted from submitting Customers who reside in non-permitted jurisdictions. The Platform will receive an error that looks like:
{
"errors": [
"The submitting platform is not allowed to operate in the participant's resident state",
"participant is not in an allowed jurisdiction"
]
}See more detail on permitted jurisdictions here.
The preferred approach is for the Platform to not allow customers to onboard on their side (through a feature flag, for example). However, if a request with a Customer in a blocked jurisdiction does get submitted to zerohash via API, the Platform should fail gracefully and display a descriptive error message on-screen.
Whitelist a self-custody sending wallet
A deposit from an address already on the allow list with sufficient information does not undergo additional Travel Rule screening, so it never enters remediation. This is the most effective way to avoid holds entirely, and can be done using the POST /travel-rule/submit endpoint. Example request:
{
"address": "0xABC123",
"participant_code": "MERCH1",
"address_owner_type": "business",
"business_pii": {
"legal_entity_name": "Business XYZ",
"legal_entity_identifier": "123456789",
"legal_entity_address": "1 Example St, Dublin"
},
"wallet_type": "self_custody"
}Find more information on Travel Rule here.
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
}Deposit outcomes and webhooks
Platforms should be listening to the following webhooks for deposit outcomes:
| Scenario | Outcome | Notifications |
|---|---|---|
| Deposit received at the address; credited | The End Customer’s crypto balance is credited. | Deposit/Withdraw webhook with state = completed |
| Deposit received; Missing travel rule information on sending wallet | Standard Travel Rule collateral flow: a 48-hour remediation window.
| Deposit/Withdraw webhook with state = pending_compliance_review |
| Deposit received; Missing travel rule information on sending wallet; 48 hour timer runs out and information has not been recovered | Funds are not credited to the sender's available participant account balance. The sender can recover funds from the zerohash europe branded secondary portal. | Deposit/Withdraw webhook with state = recovery_quarantine |
| Deposit received; Missing travel rule information on sending wallet; Travel Rule information submitted and rejected during review | Funds are not credited to the sender's available participant account balance. The sender can recover funds from the zerohash europe branded secondary portal. | Deposit/Withdraw webhook with state = recovery_quarantine |
| Deposit received; Platform not enabled OR End Customer not approved | Funds are held and not credited | Deposit/Withdraw webhook with state = quarantined |
5. Get Quote
EUR quoted currencyFor zerohash B.V. (EU), you can quote in USD or EUR. The examples on this page use USD. To quote in EUR, set the quoted currency to EUR in your request. EUR pairs are available for a growing set of assets. Visit Supported Instruments (RFQ) to see which assets can be quoted in EUR.
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
feesobject 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", //defaults to USD, EUR also supported
"quantity": "1",
"fees": [
{ "amount": "2.50", "name": "processing-1" }
]
}- Use
quoted_currencyto specify quoted currency (EUR vs USD).
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 oftotaland 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: ZHDSEUaccount_group: [platformparticipant_code]account_label: generalasset: USDaccount_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": "ZHDSEU",
"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:
- Certain fields and values, which can be obtained using our API
- Adequate support contact information
Fields
Here are the required field names and their associated API fields:
| Email Receipt Field Name | Description | Example | API Location |
|---|---|---|---|
| Order Number | Unique order identifier | 9a738372-0855-4b25-8c65-5de0aa858b8b | trade_id from GET /trades |
| Order Type | The type of order | Off Ramp Transaction | N/a, can hard-code to "Off Ramp Transaction Transmission" |
| Transmission Amount | The transmission amount for the transaction, in quoted currency terms | $10 | total from POST /liquidity/rfq or POST /convert_withdraw/execute response |
| Amount Received by End Customer | The amount of the underlying crypto received by the End Customer | 10 USDC | quantity 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/Time | Date 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:00 | settled_timestamp from GET /trades |
| Account ID | zerohash's unique account identifier for the End Customer ( the “participant code” within zerohash). | CUST01 | participant_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:
| Session | Start | End | Expected Settlement Time* |
|---|---|---|---|
| Monday | Monday 9:00a EST | Tuesday 8:59:59a EST | Tuesday EOD |
| Tuesday | Tuesday 9:00a EST | Wednesday 8:59:59a EST | Wednesday EOD |
| Wednesday | Wednesday 9:00a EST | Thursday 8:59:59a EST | Thursday EOD |
| Thursday | Thursday 9:00a EST | Friday 8:59:59a EST | Friday EOD |
| Friday | Friday 9:00a EST | Monday 8:59:59a EST | Monday 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: generalaccount_type: availableasset: [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
}
]
}Updated 3 days ago