Enterprise Settlements API Integration Guide

Seamlessly and compliantly disperse stablecoins to merchant customers through the zerohash API

API Solution

Companies that serve business customers (e.g., PSPs supporting merchants) can use the Enterprise Settlements API Solution to programmatically convert fiat to stablecoins and settle funds on-chain to merchant-owned wallets. This solution follows the same end-to-end settlement flow as the no-code experience, while giving teams full control through APIs and requiring engineering integration.


1. Submit Merchant (KYB)

Companies that use this product will be called "Platforms". Platforms will be responsible for onboarding their Merchant customers to zerohash programmatically through the API.

For the following examples, assume the Platform's platform code is PLAT01.


Merchant Information

The Platform submits the Merchant via POST /participants/entity/new

{
    "platform_code": "PLAT01",
    "entity_name": "Merchant A",
    "legal_name": "Merchant A Inc.",
    "contact_number": "15553765432",
    "website": "www.merchant.com",
    "date_established": "2018-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",
    "risk_vendor": "passbase",
    "sanction_screening": "pass",
    "sanction_screening_timestamp":1677252628000,
    "metadata":{},
    "signed_timestamp":1677252629000,
    "submitter_email": "[email protected]",
    "submitter_first_name": "Josh"  // new addition to these specs 11.6.24 (to be a required field in the future)
    "submitter_last_name": "Doe"  // new addition to these specs 11.6.24 (to be a required field in the future)
    "submitter_title": "Senior Legal Council" // new addition to these specs 11.6.24 (to be a required field in the future)
    "control_persons":[
      {
        "name": "Joe Doe",
        "email": "[email protected]",
        "address_one": "1 South St.",
        "address_two": "Suite 2000",
        "city": "Chicago",
        "postal_code": "12345",
        "jurisdiction_code": "US-IL",
        "date_of_birth": "1980-01-30",  
        "citizenship_code": "US", 
        "tax_id": "123456789",
        "id_number_type": "us_passport",
        "id_number": "332211200",
        "kyc": "pass",
        "kyc_timestamp": 1630623005000,
        "sanction_screening":"pass",
        "sanction_screening_timestamp":1677252628000,
        "control_person": 1
      }
    ],
    "beneficial_owners":[
      {
        "name": "Jane Doe Jr",
        "beneficial_owner":1,
        "email": "[email protected]",
        "address_one": "1 North St.",
        "address_two": "Suite 3000",
        "city": "Chicago",
        "postal_code": "12345",
        "jurisdiction_code": "US-IL",
        "date_of_birth": "1980-01-30",
        "citizenship_code": "US", 
        "tax_id": "012345578",
        "id_number_type": "us_drivers_license",
        "id_number": "P11122243333",
        "kyc": "pass",
        "kyc_timestamp": 1630623005000,
        "sanction_screening": "pass",
        "sanction_screening_timestamp":1677252628000
      }
    ]
}

You’ll receive a participant_code in the response - this is the Merchant participant code that uniquely identifies the entity indefinitely. We’ll use MERCH01 as the participant_code throughout the examples.

See response for expected shape.

The Platform must submit at least 1 beneficial_owners and 1 control_persons. If these persons do not have an SSN, the Platform must submit a document via POST /participants/entity/documents.


Merchant Documents

Your Merchant will not become approved unless you also supply the proper documents via POST /participants/entity/documents endpoint. Depending on the entity_type that was used in the original POST /participants/entity/new call, the document requirements vary. See details here.

The Platform submits the Merchant documents via POST /participants/entity/documents:

{
    "document": "...", // base 64 encoded file that you wish to upload (10mb limit)
    "mime": "image/png",
    "document_type": "articles_of_incorporation",
    "file_name": "test.png",
    "participant_code": "MERCH01"
}

2. Fund float account

The Float Account is used to fund instant conversions of USD to stablecoins, as zerohash does not extend credit.

The Platform will fund the Float Account by sending fiat to the proper bank account, tagging each wire with a memo equal to the Platform's participant_code. Including the wire memo means that we can auto-credit the correct account when we receive the wire/ACH.

Here are the account details that correspond to our API:

Float Account
Participant code00SCXM
Account group[Platform's participant code]
Account labelgeneral
Account typeAvailable
AssetUSD
Wire MemoPLAT01 - FLOAT

3. Whitelist Merchant destination addresses via API

Once a Merchant has been onboarded to zerohash, the platform can whitelist their wallet addresses for payouts using the zerohash API.

The Platform will use the Merchant's participant_code to link an external account via POST /payments/external_accounts:

{
	"participant_code": "MERCH01",
	"type": "crypto", // enum: crypto, fiat
	"details": {
		"network": "ETH",
		"supported_assets": ["USDC"],
		"address": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
	}
}

POST /payments/external_accounts response:

{
    "request_id": "a9e2c6fb-f738-4ecb-986c-befd70678707",
    "external_account_id": "107e8a2a-c835-4b76-b49d-a633d45727b9",
    "participant_code": "MERCH01",
    "platform_code": "PLAT01",
    "account_nickname": "",
    "created_at": "2024-10-11T00:52:21.865Z",
    "status": "pending",
    "type": "crypto",
    "details": {
        "network": "ETH",
        "supported_assets": [
            "USDC"
        ],
        "address": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
        "destination_tag": ""
    }
}

Create External Account Rejection Scenarios

ScenarioResponse
Merchant is not in an approved status"participant is not approved"
Merchant participant_code does not exist"participant {participant_code} not found"
Invalid supported_assets and/or network. The supported_asset.name/network combination results in a supported Payout asset. Official list of supported currency/networks:
• USDC.ALGO
• USDC.ARBITRUM
• USDC.AVAX
• USDC.AVAX
• USDC.BASE
• USDC.ETH
• USDC.HBAR
• USDC.OPT
• USDC.POLYGON
• USDC.SOL
• USDC.SUI
• USDC.XLM
• USDC.ZKSYNC
"{supported_assets.network} is not supported"
address is within the zerohash-maintained blacklist (ie, OFAC sanctioned address)"invalid or denied destination address"

Note: Each Merchant can have a maximum of 100 external accounts


Query Merchants' External Accounts

Platforms can view all previously-connected external accounts via GET /payments/external_accounts.


4. Initiate transactions

The Platform can initiate payout transactions via the POST /payments endpoint:

You can add an optional free-form description field, have left it out of the example.


{
"participant_code": "MERCH01",
"obo_participant": {
      "participant_code": "PAYOR1",
      "account_group": "PLAT01",
      "account_label": "general"
 	},
"external_account_id": "2cc93b20-ee43-4877-8cdc-863e61829015",
"asset":"USDC", // note: do not specify the network (ie USDC.BASE). the network is implied by the external account
"quoted_asset": "USD",
"total": "125.50",
"payment_type": "payout",
"description": ""
}

Note:

  • The participant_code will be the Merchant
  • zerohash will assume USD is being used to fund the trade to start
  • zerohash will validate on the following:
    • The external_account_id must have the specified asset attached to it
    • The platform will not have to pass a value for network on the POST /payments, it can be inferred from the account

Response example:

{
  "request_id": "0f68333e-2114-469d-b505-c850d776e063",
  "participant_code": "MERCH01",
  "obo_participant": {
      "participant_code": "PAYOR1",
      "account_group": "PLAT01",
      "account_label": "general"
    },
  "payment_id": "0f68333e-2114-469d-b505-c850d776e061",	
  "asset": "USDC",
  "network": "ETH",
  "total": "125.50",
  "quoted_asset": "USD",
  "status": "submitted",
  "external_account_id": "2cc93b20-ee43-4877-8cdc-863e61829015",
  "created_at": "20243-08-19T23:15:05.000Z"
}

Note:

  • The payment_id field represents the zerohash payment UUID, not the blockchain hash.
  • The payout will initially enter a status of submitted. The address will be run through various address checks once again. If it passes, the payout will continue. Else, the status will become failed with a failure_reason of “address_failed_check”.

Initiate Payout Rejection Scenarios

ScenarioResponse
external_account_id is not in an approved status "account is not approved"
external_account_id does not exist"account not found"
address is within the zerohash-maintained blacklist (ie, OFAC sanctioned address). in this scenario, the status of the external_account_id will remain in an approved state"invalid or denied destination address"
Merchant is not in an approved status"participant is not approved"
Merchant (participant_code) does not exist"participant {participant_code} not found"
Payor (obo_participant_code.participant_code) is not in an approved status"participant is not approved"
Payor does not exist"participant {participant_code} not found"
total not valid
• Must be a number
• Numbers after decimal must be a maximum of the precision supported by the currency associated with the external_account_id (see here for precision details)
"missing valid value(s) for required field(s): Total"
payment_type not valid
• must be payout
"payment_type should be equal to one of the allowed values"
Float account balance is insufficient compared to the Payout amount"insufficient balance"
Invalid quoted currency (USD is only supported at the moment)"invalid quoted asset"
The asset must be a supported_asset in the external account"asset not supported"

5. zerohash converts USD to stablecoins and sends stablecoins on-chain

Under the hood, zerohash will create a trade, which will take US dollars from the Payor USD Account, account details:

  • Participant_code: PAYOR1
  • Account_group: PLAT01
  • Account_label: general
  • Asset: USD
  • Amount: 100,000

And convert them to crypto, resulting in a credit to the Payor USDC account, account details:

  • Participant_code: PAYOR1
  • Account_group: PLAT01
  • Account_label: general
  • Asset: USDC
  • Amount: 125.50

zerohash will then initiate a withdrawal of the crypto from the Payor USDC account to the address associated with the external_account_id, associated with the payout (payment_id).

Immediately after the withdrawal, the payout will enter a status of posted - signifying that the USDC funds (ie, the withdrawal) has been settled on the zerohash ledger and the transaction has been dispersed on-chain.

Once the withdrawal has confirmed on-chain, the payout will transition to its final status of settled.


Payouts Webhook Examples

submitted example:

{
   "payment_id":"0f68333e-2114-469d-b505-c850d776e061",
   "obo_participant":{
      "participant_code":"PAYOR1",
      "account_group":"PLAT01",
      "account_label":"general"
   },
   "payment_details":{
      "withdrawal_request_id":"",
      "trade_id":"b752503c-1c42-4dfe-ad1d-7b39da5db59c",
      "on_chain_transaction_id":"",
      "network_fee_notional":"",
      "network_fee_quantity":"",
      "destination_address":"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
   },
   "asset":"USDC",
   "network":"ETH",
   "payment_type":"payout",
   "external_account_id":"2cc93b20-ee43-4877-8cdc-863e61829015",
   "participant_code":"MERCH01",
   "quantity":"",
   "status":"submitted",
   "created_at":"2024-09-26T13:05:22.657Z",
   "updated_at":"2024-09-26T13:05:22.657Z",
   "total":"125.50"
}

posted example:

{
   "payment_id":"0f68333e-2114-469d-b505-c850d776e061",
   "obo_participant":{
      "participant_code":"PAYOR1",
      "account_group":"PLAT01",
      "account_label":"general"
   },
   "payment_details":{
      "withdrawal_request_id":"14f8ebb8-7530-4aa4-bef9-9d73d56313f3",
      "trade_id":"b752503c-1c42-4dfe-ad1d-7b39da5db59c",
      "on_chain_transaction_id":"FLaUcxdNxRwnaSXp6pXeRSfANXEHouqYbqF6X1bgRxg2",
      "network_fee_notional":".01",
      "network_fee_quantity":".001",
      "destination_address":"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
   },
   "asset":"USDC",
   "network":"ETH",
   "payment_type":"payout",
   "external_account_id":"2cc93b20-ee43-4877-8cdc-863e61829015",
   "participant_code":"MERCH01",
   "quantity":"",
   "status":"posted",
   "created_at":"2024-09-26T13:05:22.657Z",
   "updated_at":"2024-09-26T13:05:22.657Z",
   "total":"125.50"
}  

settled example:

{
   "payment_id":"0f68333e-2114-469d-b505-c850d776e061",
   "obo_participant":{
      "participant_code":"PAYOR1",
      "account_group":"PLAT01",
      "account_label":"general"
   },
   "payment_details":{
      "withdrawal_request_id":"14f8ebb8-7530-4aa4-bef9-9d73d56313f3",
      "trade_id":"b752503c-1c42-4dfe-ad1d-7b39da5db59c",
      "on_chain_transaction_id":"FLaUcxdNxRwnaSXp6pXeRSfANXEHouqYbqF6X1bgRxg2",
      "network_fee_notional":".01",
      "network_fee_quantity":".001",
      "destination_address":"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
   },
   "asset":"USDC",
   "network":"ETH",
   "payment_type":"payout",
   "external_account_id":"2cc93b20-ee43-4877-8cdc-863e61829015",
   "participant_code":"MERCH01",
   "quantity":"",
   "status":"settled",
   "created_at":"2024-09-26T13:05:22.657Z",
   "updated_at":"2024-09-26T13:05:22.657Z",
   "total":"125.50"
}    

failed example:

{
   "payment_id":"0f68333e-2114-469d-b505-c850d776e061",
   "obo_participant":{
      "participant_code":"PAYOR1",
      "account_group":"PLAT01",
      "account_label":"general"
   },
   "payment_details":{
      "withdrawal_request_id":"14f8ebb8-7530-4aa4-bef9-9d73d56313f3",
      "trade_id":"b752503c-1c42-4dfe-ad1d-7b39da5db59c",
      "on_chain_transaction_id":"FLaUcxdNxRwnaSXp6pXeRSfANXEHouqYbqF6X1bgRxg2",
      "network_fee_notional":".01",
      "network_fee_quantity":".001",
      "destination_address":"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
   },
   "asset":"USDC",
   "network":"ETH",
   "payment_type":"payout",
   "external_account_id":"2cc93b20-ee43-4877-8cdc-863e61829015",
   "participant_code":"MERCH01",
   "quantity":"",
   "status":"failed",
   "created_at":"2024-09-26T13:05:22.657Z",
   "updated_at":"2024-09-26T13:05:22.657Z",
   "total":"125.50"
}   

Network Fee Procedure

The Platform will be paying for the network fee associated with any payouts. There are 2 options:

  1. Fund the network fee account. Account details:
  • Participant_code: PLAT01 (the Platform's participant_code)
  • Account_group: PLAT01
  • Account_label: network_fee
  • Asset: USD

All network fees will paid out of this account in real-time

  1. Don’t fund the network fee account. This account will build up a payable balance

zerohash will add the amount of the receivable balance on this account to the end of month invoice, coupling it with the payout usage activity amount due.


Email Receipt Requirements

zerohash requires that the Payor receives an email receipt for each payout. We also insist that the email contain

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

Required Fields:

Email Receipt Field NameDescriptionExampleAPI Location
Order NumberUnique order identifier9a738372-0855-4b25-8c65-5de0aa858b8bpayment_id from GET /payments or POST /payments response
Order TypeThe type of orderPayout Transmission

(must present this value verbatim)
N/a, can hard-code to "Payout Transmission"
Transmission AmountThe transmission amount for the payout, in quoted currency terms$10total GET /payments or POST /payments response
Amount Received by MerchantThe amount of the underlying payout received by the Merchant10 USDCtotal GET /payments or POST /payments response
FeesAny added fee values included in the Payout. 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 Payor) or blockchain network fees.
"$0", "1.99", "$0 Fees", or "No Fees"- Payouts does not currently support processing fees

- If the Payor is incurring the network fee, take network_fee_notional from [GET /payments]. If not, no need to include it in the receipt
Date/TimeDate and timestamp of the payout's transmission (meaning the time that the fiat was converted and settled into USDC within the zerohash system)2024-10-11 18:38:00created_at from GET /payments or POST /payments response
Account IDzerohash’s unique account identifier for the Payor ( the “participant code” within zerohash).BENEF1participant_code from GET /payments or POST /payments response

Support Information:

FieldValue (examples)
Platform Contact Email [email protected]
Platform Phone(123) 123-1234
Platform Address 123 Main Street
New York, NY 10014
Platform Support Contact Email (if different from above)[email protected] - this is an email that is used to contact zerohash directly
zerohash Contact InformationZero Hash LLC (NMLS ID 1699379)
327 N Aberdeen St
Chicago, IL 60607
855-744-7333
[email protected]
www.zerohash.com

This value should be included in the email verbatim

6. Complete end of day settlement (T+1)

It is a regulatory requirement of zerohash's for the Platform to perform settlement on a T+1 basis. For example, if a Platform executed $1,000,000 worth of payouts on Monday, on Tuesday we expect a wire of $1,000,000 in order to consider Monday's settlement "closed".


7. View transaction activity

The Platform can view all payouts via GET /payments (shown initially in a submitted status below):

{
  "message": [
    {
      "payment_id": "0f68333e-2114-469d-b505-c850d776e061",
      "obo_participant": {
         "participant_code": "PAYOR1",
         "account_group": "PLAT01",
         "account_label": "general"
      },      
      "payment_details": {
         	"withdrawal_id": "",
	        "trade_id": "",
	        "on_chain_transaction_id ": "",
          "network_fee_notional": "",
          "network_fee_quantity": "",
      }
      "asset": "USDC",
      "network": "ETH",
      "payment_type":"payout",
      "external_account_id": "2cc93b20-ee43-4877-8cdc-863e61829015"
      "participant_code": "MERCH01",
      "amount": "125.50",
      "status": "submitted",
      "failure_reason": "",
      "created_at": "2024-08-19T23:15:30.000Z",
      "updated_at": "2024-08-19T23:15:35.000Z"
    }
  ]
}

Platforms can also query an individual payout via the GET /payments/id endpoint. Example:

{
  "message":
    {
      "payment_id": "0f68333e-2114-469d-b505-c850d776e061",
      "obo_participant": {
         "participant_code": "PAYOR1",
         "account_group": "PLAT01",
         "account_label": "general"
      },      
      "payment_details": {
         	"withdrawal_id": "",
	        "trade_id": "",
	        "on_chain_transaction_id ": "",
          "network_fee_notional": "",
          "network_fee_quantity": "",
      }
      "asset": "USDC",
      "network": "ETH",
      "payment_type":"payout",
      "external_account_id": "2cc93b20-ee43-4877-8cdc-863e61829015"
      "participant_code": "MERCH01",
      "amount": "125.50",
      "status": "submitted",
      "failure_reason": "",
      "created_at": "2024-08-19T23:15:30.000Z",
      "updated_at": "2024-08-19T23:15:35.000Z"
    }
}

When the withdrawal has settled on the zerohash ledger and directly after the on-chain transfer is sent on-chain, the Payout will transition to a posted status and will have non-null values for withdrawal_request_id, trade_id, on_chain_transaction_id, network_fee_notional, and network_fee_quantity:

{
  "message": [
    {
      "payment_id": "0f68333e-2114-469d-b505-c850d776e061",
      "obo_participant": {
         "participant_code": "PAYOR1",
         "account_group": "PLAT01",
         "account_label": "general"
    },      
      "payment_details": {
          	"withdrawal_request_id": "abc123",
          	"trade_id": "def456",
	          "on_chain_transaction_id ": "FLaUcxdNxRwnaSXp6pXeRSfANXEHouqYbqF6X1bgRxg2",
            "network_fee_notional": ".01",
            "network_fee_quantity": ".001",
            "destination_address": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
}
      "asset": "USDC",
      "network": "ETH",
      "payment_type":"payout",
      "external_account_id": "2cc93b20-ee43-4877-8cdc-863e61829015"
      "participant_code": "MERCH01",
      "amount": "125.50",
      "status": "posted",
      "failure_reason": "",
      "created_at": "2024-08-19T23:15:30.000Z",
      "updated_at": "2024-08-19T23:15:38.000Z"
    }
  ]
}

When the withdrawal has been confirmed on-chain, the payout will transition to a settled status. Example payload:

{
  "message": [
    {
      "payment_id": "0f68333e-2114-469d-b505-c850d776e061",
      "obo_participant": {
         "participant_code": "PAYOR1",
         "account_group": "PLAT01",
         "account_label": "general"
    },      
      "payment_details": {
          	"withdrawal_request_id": "abc123",
          	"trade_id": "def456",
	          "on_chain_transaction_id ": "FLaUcxdNxRwnaSXp6pXeRSfANXEHouqYbqF6X1bgRxg2",
            "network_fee_notional": ".01",
            "network_fee_quantity": ".001",
            "destination_address": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
}
      "asset": "USDC",
      "network": "ETH",
      "payment_type":"payout",
      "external_account_id": "2cc93b20-ee43-4877-8cdc-863e61829015"
      "participant_code": "MERCH01",
      "amount": "125.50",
      "status": "settled",
      "failure_reason": "",
      "created_at": "2024-08-19T23:15:30.000Z",
      "updated_at": "2024-08-19T23:15:38.000Z"
    }
  ]
}