Onboarding API + Fund API

See core product page here

Setup

In this guide, the Platform is setup to use the Onboarding API (Reliance) plus the Fund API.

Flow

The following example illustrates a platform configuration where incoming stablecoin deposits are automatically converted to USD and subsequently swept to the Platform’s master ledger account.

Webhook configuration

Talk to your zerohash representative to have your Platform configured for the following webhooks:

  • Onboarding
  • Account Funding

Submit customer

📘

Natural vs. Non-natural person

Platforms can choose to onboard end users (ie, Natural Persons) or entities (ie, Non-natural persons). Both examples will be shown below

Natural Person

Example request - POST /participants/customers/new

{
   "first_name":"John",
   "last_name":"Smith",
   "email":"[email protected]",
   "phone_number":"9545551234",
   "address_one":"1 Main St.",
   "address_two":"Suite 1000",
   "city":"Chicago",
   "state":"IL",
   "zip":"12345",
   "country":"United States",
   "date_of_birth":"1985-09-02",
   "citizenship":"United States",
   "tax_id":"123456789",
   "risk_rating":"low",
   "kyc":"pass",
   "kyc_timestamp":1630623005000,
   "sanction_screening":"pass",
   "sanction_screening_timestamp":1630623005000,
   "idv":"pass",
   "liveness_check":"pass",
   "signed_timestamp":1630623005000,
   "metadata":{
      
   },
   "signed_agreements":[
      {
         "type":"fund_auto_convert",
         "region":"us",
         "signed_timestamp":1712008721000
      }
   ]
}

If you fail to indicate that the end customer has agreed to the account funding-specific terms, in the signed_agreements object shown above, /fund API calls will fail.

There may be situations where the Platform is restricted from submitting customers who reside in certain jurisdictions (ie, New York). 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"
    ]
}

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.

Non-natural person

📘

Partial vs. Full Non-natural person

Partial: zerohash offers approved Platforms the ability to submit a stripped-down KYB data packet. This is typically reserved for licensed financial institions, but is subject to review on a case-by-case basis.

Full: default comprehensive KYB data packet

Partial Non-natural person

To submit a partial non-natural person customer, use the POST /participants/entity/new endpoint. Example request:

{    
    "request_id": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
    "platform_code": "PLAT01",
    "legal_name": "Entity A",
    "contact_number": "15553765432",
    "address_one": "1 Main St.",
    "address_two": "Suite 1000",
    "city": "Chicago",
    "postal_code": "12345",
    "jurisdiction_code": "US-IL",
    "tax_id": "883987654",
    "id_issuing_authority": "United States",
    "sanction_screening": "pass",
    "sanction_screening_timestamp": 1603378501286,
    "signed_agreements": [
  {
    "type": "fund_auto_convert",
    "region": "us",
    "signed_timestamp": 1603378501286
  }
]  
}

Example response:

{
  "message": {
    "platform_code": "PLAT01",
    "participant_code": "CUST01",
    "status": "approved",
    "entity_name": "Entity A",
    "legal_name": "Entity A",
    "subdomain": "",
    "address_one": "1 Main St.",
    "address_two": "Suite 1000",
    "country": "United States",
    "state_or_province": "",
    "jurisdiction_code": "US-IL",
    "city": "Chicago",
    "postal_code": "12345",
    "date_established": "",
    "risk_rating": "",
    "risk_vendor": "",
    "entity_type": "",
    "metadata": {},
    "signed_timestamp": 0,
    "tax_id": "883987654",
    "contact_number": "15553765432",
    "website": "",
    "id_issuing_authority": "United States",
    "sanction_screening": "pass",
    "sanction_screening_timestamp": 1603378501000,
    "expected_annual_volume": "",
    "submitter_email": "",
    "submitter_first_name": "",
    "submitter_last_name": "",
    "submitter_title": "",
    "beneficial_owners": [],
    "control_persons": [],
    "id_number_type": "",
    "id_number": "",
    "self_certification_timestamp": 0,
    "signed_agreements": [
      {
        "type": "fund_auto_convert",
        "region": "us",
        "signed_timestamp": 1603378501286
      }
    ]
  }
}
Full Non-natural person

To submit a full non-natural person customer, use the POST /participants/entity/new endpoint. Example request:

{
  "request_id": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
  "platform_code": "PLAT01",
  "entity_name": "Entity A",
  "legal_name": "Entity A",
  "contact_number": "15553765432",
  "website": "https://entitya.com",
  "date_established": "2020-01-15",
  "entity_type": "llc",
  "address_one": "1 Main St.",
  "address_two": "Suite 1000",
  "city": "Chicago",
  "postal_code": "12345",
  "jurisdiction_code": "US-IL",
  "tax_id": "883987654",
  "id_issuing_authority": "United States",
  "risk_rating": "low",
  "sanction_screening": "pass",
  "sanction_screening_timestamp": 1603378501286,
  "signed_timestamp": 1603378501286,
  "submitter_email": "[email protected]",
  "control_persons": [
    {
      "name": "Jane Smith",
      "first_name": "Jane",
      "last_name": "Smith",
      "email": "[email protected]",
      "address_one": "1 Main St.",
      "city": "Chicago",
      "postal_code": "12345",
      "jurisdiction_code": "US-IL",
      "citizenship_code": "US",
      "date_of_birth": "1985-06-15",
      "tax_id": "123456789",
      "id_number_type": "ssn",
      "id_number": "123456789",
      "id_issuing_authority": "United States",
      "sanction_screening": "pass",
      "sanction_screening_timestamp": 1603378501286,
      "control_person": 1,
      "kyc": "pass",
      "kyc_timestamp": 1603378501286
    }
  ],
  "beneficial_owners": [
    {
      "name": "Jane Smith",
      "first_name": "Jane",
      "last_name": "Smith",
      "email": "[email protected]",
      "address_one": "1 Main St.",
      "city": "Chicago",
      "postal_code": "12345",
      "jurisdiction_code": "US-IL",
      "citizenship_code": "US",
      "date_of_birth": "1985-06-15",
      "tax_id": "123456789",
      "sanction_screening": "pass",
      "sanction_screening_timestamp": 1603378501286,
      "beneficial_owner": 25,
      "kyc": "pass",
      "kyc_timestamp": 1603378501286
    }
  ],
  "signed_agreements": [
    {
      "type": "fund_auto_convert",
      "region": "us",
      "signed_timestamp": 1603378501286
    }
  ]
}

Example response:

{
  "message": {
    "platform_code": "PLAT01",
    "participant_code": "WFG012",
    "status": "submitted",
    "entity_name": "Entity A",
    "legal_name": "Entity A",
    "subdomain": "",
    "address_one": "1 Main St.",
    "address_two": "Suite 1000",
    "country": "",
    "state_or_province": "",
    "jurisdiction_code": "US-IL",
    "city": "Chicago",
    "postal_code": "12345",
    "date_established": "2020-01-15",
    "risk_rating": "low",
    "risk_vendor": "unknown",
    "entity_type": "llc",
    "metadata": {},
    "signed_timestamp": 1603378501000,
    "tax_id": "883987654",
    "contact_number": "15553765432",
    "website": "https://entitya.com",
    "id_issuing_authority": "United States",
    "sanction_screening": "pass",
    "sanction_screening_timestamp": 1603378501000,
    "expected_annual_volume": "unknown",
    "submitter_email": "[email protected]",
    "submitter_first_name": "",
    "submitter_last_name": "",
    "submitter_title": "",
    "id_number_type": "",
    "id_number": "",
    "self_certification_timestamp": 0,
    "control_persons": [
      {
        "user_code": "USHJ7K",
        "first_name": "Jane",
        "middle_name": "",
        "last_name": "Smith",
        "name": "Jane Smith",
        "email": "[email protected]",
        "address_one": "1 Main St.",
        "address_two": "",
        "city": "Chicago",
        "state_or_province": "",
        "postal_code": "12345",
        "country": "",
        "jurisdiction_code": "US-IL",
        "date_of_birth": "1985-06-15",
        "place_of_birth_name": "",
        "place_of_birth_country_code": "",
        "citizenship": "",
        "citizenship_code": "US",
        "tax_id": "123456789",
        "id_number_type": "ssn",
        "id_number": "123456789",
        "id_issuing_authority": "United States",
        "sanction_screening": "pass",
        "sanction_screening_timestamp": 1603378501000,
        "role": "",
        "control_person": 1,
        "kyc": "pass",
        "kyc_timestamp": 1603378501286
      }
    ],
    "beneficial_owners": [
      {
        "user_code": "BOWN3R",
        "beneficial_owner": 25,
        "first_name": "Jane",
        "middle_name": "",
        "last_name": "Smith",
        "name": "Jane Smith",
        "email": "[email protected]",
        "address_one": "1 Main St.",
        "address_two": "",
        "city": "Chicago",
        "state_or_province": "",
        "postal_code": "12345",
        "country": "",
        "jurisdiction_code": "US-IL",
        "date_of_birth": "1985-06-15",
        "place_of_birth_name": "",
        "place_of_birth_country_code": "",
        "citizenship": "",
        "citizenship_code": "US",
        "tax_id": "123456789",
        "id_number_type": "ssn",
        "id_number": "",
        "id_issuing_authority": "",
        "sanction_screening": "pass",
        "sanction_screening_timestamp": 1603378501000,
        "role": "",
        "kyc": "pass",
        "kyc_timestamp": 1603378501286
      }
    ],
    "signed_agreements": [
      {
        "type": "fund_auto_convert",
        "signed_timestamp": 1603378501,
        "region": "us"
      }
    ],
    "b_notice_receipt": false,
    "is_w_form_certified": false,
    "physical_delivery": false,
    "signature": "",
    "payee_exemption": 0,
    "is_not_subject_backup_withholding": false,
    "fatca_reporting_exemption": 0,
    "dba_name": "",
    "other_entity_type": ""
  }
}
Idempotency

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

Create deposit address

To create a deposit address, use the hit the POST /fund/rfq endpoint. Example request:

{
   "participant_code":"CUST01",
   "fund_asset":"USDC.ETH",
   "client_fund_id":"abc123"
}

Notes:

  • See client_fund_id release notes on behavior here.

Example response:

{
  "message": {
    "request_id": "14f8ebb8-7530-4aa4-bef9-9d73d56313f3",
    "participant_code": "CUST01",
    "fund_asset": "USDC.ETH",
    "rate": "1",
    "quoted_currency": "USD",
    "expiry_timestamp": null,
    "deposit_address": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
    "deposit_fee_bps": 100,
    "subsequent_deposit_fee_floor": "1.00",
    "first_deposit_fee_floor": "1.00",
    "minimum_deposit": "1",
    "maximum_deposit": "500000",
    "reference_id": "abc123"
  }
}

Customer deposits crypto or stablecoins

You should now be prompting the customer to deposit to the deposit_address from the above response. All deposits made to this address will be auto-converted to USD and transferred to your account on our ledger.

Deposit received

The transaction has received the proper amount of on-chain confirmations (for USDC in this case, it is 1) and zerohash initiates a series of movements:

Accounts for this example:

account_idaccount_owneraccount_groupaccount_labelaccount_typeasset
47a854fa-d0e4-405b-831b-f1a86bbf7988CUST01PLAT01generalavailableUSDC.ETH
52c29f2d-8298-4e8c-84bc-7773960bee12CUST01PLAT01generalavailableUSD
  1. Deposit: Using our account nomenclature, the movement would appear as:
account_owneraccount_groupaccount_labelaccount_typemovement_typeassetchange
CUST01PLAT01generalavailabledepositUSDC.ETH+1,000
  1. Conversion: Automatically, zerohash will convert the USDC to USD. This technically creates a trade, which has a movement_type of final_settlement:
account_owneraccount_groupaccount_labelaccount_typemovement_typeassetchange
CUST01PLAT01generalavailablefinal_settlementUSDC.ETH-1,000
CUST01PLAT01generalavailablefinal_settlementUSD+1,000
  1. Transfer: Automatically, zerohash will transfer the USD from the end customer account to the platform's:
account_owneraccount_groupaccount_labelaccount_typemovement_typeassetchange
CUST01PLAT01generalavailabletransferUSD-1,000
PLAT01PLAT01generalavailabletransferUSD+1,000

Webhook event triggered

You will then receive a webhook with payload type = DEPOSIT_FUND_COMPLETE. Example payload:

{
  "participant_code": "CUST01",
  "fund_asset": "USDC.ETH",
  "rate": "1",
  "quoted_currency": "USD",
  "source_address": "3xJ9KzymPqfHBqp2fGKoHtBcEn7LP5gSYNzGKS1vJcBr",
  "deposit_address": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
  "quantity": "500",
  "notional": "495.00",
  "fund_id": "a1b2c3d4-5678-9012-abcd-ef1234567890",
  "fund_timestamp": 1745262000000000000,
  "deposit_timestamp": 1745261950000000000,
  "transaction_id": "4vJ9GKzymPqfHBqp2fGKoHtBcEn7LP5gSYNzGKS1vJcBr2xJ9KzymPqfHBqp2fG",
  "account_label": "general",
  "success": true,
  "reason": "Deposit processed",
  "reference_id": "abc123",
  "raw_fee_bps": "100",
  "deposit_fee_bps": "100",
  "raw_fee_notional": "5.00",
  "deposit_fee_notional": "5.00",
  "source": {}
}

Query transactions

You can now also retroactively query the GET /fund/transactions endpoint to view details of prior fund events. View the code recipe for additional assistance. Example response:

{
  "message": [
    {
      "participant_code": "CUST01",
      "fund_asset": "USDC.ETH",
      "rate": "1",
      "quoted_currency": "USD",
      "source_address": "3xJ9KzymPqfHBqp2fGKoHtBcEn7LP5gSYNzGKS1vJcBr",
      "deposit_address": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
      "quantity": "500",
      "notional": "500",
      "success": true,
      "status_reason": "",
      "status_reason_code": "DEPOSIT_PROCESSED",
      "fund_timestamp": 1745262000000,
      "deposit_timestamp": 1745261950000,
      "transaction_id": "4vJ9GKzymPqfHBqp2fGKoHtBcEn7LP5gSYNzGKS1vJcBr2xJ9KzymPqfHBqp2fG",
      "account_label": "general",
      "fund_id": "a1b2c3d4-5678-9012-abcd-ef1234567890",
      "is_first_deposit": true,
      "raw_fee_bps": "100",
      "deposit_fee_bps": "100",
      "raw_fee_notional": "5",
      "deposit_fee_notional": "5",
      "deposited_asset": "USDC.SOL",
      "reference_id": "abc123"
    }
  ],
  "page": 1,
  "page_size": 50,
  "total_pages": 1
}

Reconciliation - query the movements endpoint

The platform can now query GET /movements?parent_link_id=5155f7c9-95cb-4556-ab89-c178943a7111 in order to view all related ledger movements to this fund event.

Example response - GET /movements?parent_link_id=

{
  "message": [
    {
      "movement_timestamp": 1550174570000,
      "account_id": "47a854fa-d0e4-405b-831b-f1a86bbf7988",
      "movement_id": "5d4d962d-dc37-4ddc-b0c5-b8980d1d4421",
      "movement_type": "deposit",
      "transfer_type": null,
      "deposit_reference_id": null,
      "withdrawal_request_id": null,
      "parent_link_id": "5155f7c9-95cb-4556-ab89-c178943a7111",
      "trade_id": null,
      "change": "1000"
    },
    {
      "movement_timestamp": 1550174571000,
      "account_id": "47a854fa-d0e4-405b-831b-f1a86bbf7988",
      "movement_id": "3d4d962d-dc37-4ddc-b0c5-b8980d1d4422",
      "movement_type": "final_settlement",
      "transfer_type": null,
      "deposit_reference_id": null,
      "withdrawal_request_id": null,
      "parent_link_id": "5155f7c9-95cb-4556-ab89-c178943a7111",
      "trade_id": null,
      "change": "-1000"
    },
    {
      "movement_timestamp": 1550174572000,
      "account_id": "52c29f2d-8298-4e8c-84bc-7773960bee12",
      "movement_id": "80818b7b-92ec-4882-a371-a59fb3f8bce0",
      "movement_type": "final_settlement",
      "transfer_type": null,
      "deposit_reference_id": null,
      "withdrawal_request_id": null,
      "parent_link_id": "5155f7c9-95cb-4556-ab89-c178943a7111",
      "trade_id": null,
      "change": "1000"
    },
    {
      "movement_timestamp": 1550174573000,
      "account_id": "52c29f2d-8298-4e8c-84bc-7773960bee12",
      "movement_id": "80818b7b-92ec-4882-a371-a59fb3f8bce0",
      "movement_type": "transfer",
      "transfer_type": null,
      "deposit_reference_id": null,
      "withdrawal_request_id": null,
      "parent_link_id": "5155f7c9-95cb-4556-ab89-c178943a7111",
      "trade_id": null,
    "change": "-1000"
    },
    {
      "movement_timestamp": 1550174574000,
      "account_id": "77c29f2d-8298-4e8c-84bc-7773960bee10",
      "movement_id": "80818b7b-92ec-4882-a371-a59fb3f8bce0",
      "movement_type": "transfer",
      "transfer_type": null,
      "deposit_reference_id": null,
      "withdrawal_request_id": null,
      "parent_link_id": "5155f7c9-95cb-4556-ab89-c178943a7111",
      "trade_id": null,
      "change": "1000"
    }
  ]
}

Deposit returns

🚧

IMPORTANT: You cannot rely on the deposit’s source address as the return destination. Blockchain transactions are not inherently bidirectional, and returning funds to the sender address may result in loss of funds or failed deliveries.

Some Platforms may choose to pair their zerohash integration with their own transaction monitoring tools. In situations where zerohash accepts a deposit, but the Platform’s monitoring logic flags and rejects it, the Platform may opt to return the deposit by using the POST /withdrawal/requests endpoint.

The required flow is to always request and confirm a return address directly from the customer before initiating the return.

Please speak to a zerohash sales engineer for guidance if interested in this flow.

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)

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, ie af063ca6-836f-4677-8ebf-edbe1d049938, to query GET /accounts/af063ca6-836f-4677-8ebf-edbe1d049938. 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
        }
    ]
}