Accounts Receivable Integration Guide

Generate re-usable deposit addresses for Merchants and their Third Party Payors

📘

zerohash B.V. (EU entity)

The following steps should be followed if you're onboarding a Customer to zerohash B.V. (the EU entity). For EU API calls, use base URL: https://api.zerohash.eu

ℹ️

Stablecoins only for Third Party Payor Accounts Receivable Deposits

Non-stablecoin deposits received from Third Party Payors will trigger a fund failure event for Platforms onboarded to zerohash B.V.

Overview

Platforms can use POST /fund/rfq to facilitate to two kinds of deposits:

  • Account funding (me-to-me): the Merchant funds its own account with a re-usable address for deposits they send themselves.
  • Third Party Payor deposits: the merchant's customers, called Third Party Payors, pay what they owe the merchant, for example to settle an invoice. One re-usable deposit address per Third Party Payor.

zerohash name matches each deposit's Travel Rule information against the expected Sender linked to the deposit address. Matched deposits are auto-converted and credited to the Merchant's available balance.

Key Concepts

TermDefinition
MerchantAn onboarded Non-natural Person (NNP) on your platform that receives funds. The merchant is a zerohash customer and the account owner.
Third Party PayorA business or individual that sends funds to a merchant, for example to settle an invoice. It is a separate legal entity from the merchant and is not a zerohash customer.
Expected SenderUsed throughout this guide to reference which Third Party Payor or Merchant is associated with a deposit address

Setup

In this guide, the Platform is setup to use the zerohash europe B.V. Onboarding API (Reliance) hosted by api.zerohash.eu, along with the zerohash europe B.V.

Flow

The following example illustrates a Platform configuration where incoming stablecoin deposits are name matched to the Expected Sender, then 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 via API (Reliance)

The Platform will use an external, non-zerohash KYC provider to perform proper verification of the Customer. After a successful verification, pass the results to zerohash via API.

Submit Merchant

Non-natural person

To submit the Merchant as a zerohash customer, use the POST /participants/entity/new endpoint.

Example request:

{
  "entity_type": "corporation",
  "platform_code": "PLAT01",
  "entity_name": "Entity Name",
  "legal_name": "Legal Name",
  "contact_number": "+15557778888",
  "date_established": "1985-09-02",
  "address_one": "123 Main St.",
  "city": "Paris",
  "postal_code": "12345",
  "tax_id": "000-00-0000",
  "id_issuing_authority": "US",
  "risk_rating": "low",
  "sanction_screening": "pass",
  "sanction_screening_timestamp": 1603378501286,
  "signed_timestamp": 1603378501286,
  "signed_agreements": [
      {
      "region": "eu",
      "type": "account_funding_general",
      "signed_timestamp": 1603378501286
      }
    ],
  "submitter_email": "[email protected]",
  "control_persons": [
      {
      "sanction_screening": "pass",
      "kyc": "pass",
      "name": "John Smith",
      "email": "[email protected]",
      "address_one": "123 Main St.",
      "city": "Paris",
      "postal_code": 12345,
      "jurisdiction_code": "US-IL",
      "date_of_birth": "1985-09-02",
      "phone_number": "+15557778888",
      "citizenship_code": "PT",
      "tax_id": "000-00-0000",
      "id_number_type": "eu_drivers_license",
      "id_number": 123456789,
      "id_issuing_authority": "PT",
      "sanction_screening_timestamp": 1603378501286,
      "kyc_timestamp": 1603378501286
      }
    ],
  "beneficial_owners": [
      {
      "sanction_screening": "pass",
      "kyc": "pass",
      "name": "John Smith",
      "email": "[email protected]",
      "address_one": "123 Main St.",
      "city": "Paris",
      "postal_code": 12345,
      "jurisdiction_code": "US-IL",
      "date_of_birth": "1985-09-02",
      "phone_number": "+15557778888",
      "citizenship_code": "PT",
      "tax_id": "000-00-0000",
      "id_number_type": "eu_drivers_license",
      "id_number": 123456789,
      "sanction_screening_timestamp": 1603378501286,
      "kyc_timestamp": 1603378501286
    }
  ],
  "chamber_of_commerce_number": "KVK-12345678",
  "activity_locations": [
    "PT, FR"
  ],
  "entity_source_of_funds": "crypto_activities",
  "declaration_no_foreseeable_changes": true,
  "reason_for_business_relationship": "Digital asset custody and trading services for institutional clients",
  "board_members": [
    {
      "sanction_screening": "pass",
      "sanction_screening_timestamp": 1603378501286,
      "id_issuing_authority": "US",
      "id_number": 123456789,
      "id_number_type": "us_passport",
      "tax_id": "000-00-0000",
      "citizenship_code": "US",
      "place_of_birth_country_code": "US",
      "place_of_birth_name": "Chicago",
      "date_of_birth": "1985-09-02",
      "jurisdiction_code": "US-IL",
      "postal_code": 12345,
      "city": "Chicago",
      "address_one": "123 Main St.",
      "email": "[email protected]",
      "last_name": "Smith",
      "first_name": "John"
    }
  ],
  "country": "IRL",
  "id_number": "549300TRWGQX5XMHYZ82",
  "expected_annual_volume": "over_500k",
  "id_number_type": "legal_entity_identifier"
}
'

zerohash will respond with a participant_code that uniquely identifies the customer. See more detail on EU participant creation here.

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 Prohibited 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"
    ]
}

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.

See more detail on permitted jurisdictions here.

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

Submit Third Party Payor

Non-natural person

To submit a business as a Third Party Payor, use the POST /participants/entity/new endpoint with onboarding_profile set to third_party_payor.

FieldRequiredDescription
onboarding_profileYesMust be third_party_payor
merchant_participant_codeYesParticipant code of the merchant (NNP) the payor is paying
legal_nameYesRegistered legal business name
dba_nameYesTrading or DBA name
address_oneYesLegal address, line 1
address_twoNoLegal address, line 2
cityYesCity
postal_codeYesPostal code
countryYesCountry of the legal address
emailYesBusiness email
contact_numberNoBusiness phone number
legal_entity_identifier or company_registration_number or tax_idYes, one ofLEI or Company Registration Number or TIN
incorporation_address.jurisdiction_codeYesISO 3166-2 jurisdiction of incorporation, used to validate the jurisdiction
date_establishedYesDate of incorporation
merchant_category_codeYesPayor's MCC
sanction_screeningNoSanction screening result. Approved platforms only, see below
sanction_screening_timestampNoTime the platform ran the screening. Required if sanction_screening is sent

Example Request:

{
  "onboarding_profile": "third_party_payor",
  "merchant_participant_code": "MERCH1", 
  "legal_name": "Acme Supplies B.V.",
  "dba_name": "Acme Supplies",
  "address_one": "1 Example Street",
  "city": "Amsterdam",
  "postal_code": "1011AB",
  "country": "Netherlands",
  "email": "[email protected]",
  "legal_entity_identifier": "5493001KJTIIGC8Y1R12",
  "company_registration_number": "12345678",
  "incorporation_address": { "jurisdiction_code": "NL-NH" },
  "date_established": "2015-04-01",
  "merchant_category_code": "5045",
  "request_id": "8f14e45f-ceea-4672-a0a2-5f1c3e2b9d10"
}

Natural persons

To submit an individual, use the POST /participants/customers/new endpoint with onboarding_profile set to third_party_payor.

FieldRequiredDescription
onboarding_profileYesMust be third_party_payor
partialYesMust be true (only relevant when onboarding a natural person)
merchant_participant_codeYesParticipant code of the merchant (NNP) the payor is paying
first_nameYesLegal first name of the payor
last_nameYesLegal last name of the payor
dba_nameYesTrading or DBA name (applicable only if natural person is acting as a business)
address_oneYesLegal address, line 1
address_twoNoLegal address, line 2
cityYesCity
postal_codeYesPostal code
countryYesCountry of the legal address
emailYesEmail, used for fund recovery scenarios
phone_numberNoPhone number
tax_idYesTIN, The national ID of the individual, e.g. a social security number
id_numberYesThe ID number for the individual
id_number_typeYesForm of ID associated with id_number
citizenship_codeYesIndividual's citizenship code
date_of_birthYesDate of birth
merchant_category_codeYesPayor's MCC (applicable only if natural person is acting as a business)
sanction_screeningNoSanction screening result. Approved platforms only, see below
sanction_screening_timestampNoTime the platform ran the screening. Required if sanction_screening is sent

Example Request:

{
  "onboarding_profile": "third_party_payor",
  "merchant_participant_code": "MERCH1",
  "first_name": "Jane",
  "last_name": "Doe",
  "dba_name": "Jane Doe Consulting",
  "email": "[email protected]",
  "address_one": "1 Example Street",
  "city": "Amsterdam",
  "postal_code": "1011AB",
  "citizenship_code": "NL",
  "date_of_birth": "1985-01-01",
  "id_number_type": "passport",
  "id_number": "X0000000",
  "tax_id": "000000000",
  "merchant_category_code": "5045",
  "request_id": "3c59dc04-8b2e-4f1a-9d6e-2a7b5c4e1f22"
}

For either payor type, if a required field is missing, the request fails and the response names the missing fields. The payor is not added to the allow list for that merchant. Fix the request and resubmit.

Sanction Screening

By default, zerohash runs its sanction screening controls on every Third Party Payor submitted. Approved platforms can screen payors themselves and pass the result in sanction_screening and sanction_screening_timestampfor real-time approval of the Third Party Payor. Otherwise, Third Party Payor will be in pending state until sanction screening has been completed by zerohash. Ask your zerohash representative about approval.

Whitelist a self-custody sending wallet

If the Merchant or Third Party Payor is paying from a self-custody wallet, whitelist the sending address with the POST /travel-rule/submit endpoint before they send funds. Deposits from Exchanges send Travel Rule information through the Travel Rule network, so no whitelisting is needed.

 {

  "address": "0xABC123",
  "participant_code": "MERCH1",
  "address_owner_type": "business",
  "business_pii": { 
    "legal_entity_name": "Business XYZ",
    "legal_entity_identifier": "123456789", //optional, only send when available
    "legal_entity_address": "1 Example St, Dublin"
  },
  "wallet_type": "self_custody",
  "deposit_id": "123456789" //optional, only send when referencing a specific deposit
}

Find more information on Travel Rule here.

Create a deposit address for a Third Party Payor

Each Third Party Payor must use a deposit address that is unique to them for you-to-me Accounts Receivable deposits. To create a re-usable deposit address for a Third Party Payor, use the POST /fund/rfq endpoint and specify the Expected Sender for the address using account_label.

Example Request:

{
   "participant_code":"MERCH1",
   "fund_asset":"USDC.ETH",
   "account_label":"PAYOR1",
   "client_fund_id":"abc123" //always optional
}

Notes:

  • See client_fund_id release notes on behavior here.
  • See POST /fund/rfq API reference for possible responses.

Third Party Payor deposits stablecoins

Merchants can use the deposit_address returned by POST /fund/rfq to facilitate an Accounts Receivable payment from the Expected Sender. The address doesn't expire, so the Merchant can re-use it on every invoice to the Third Party Payor linked to that address. Any amount within the standard minimum and maximum thresholds is accepted. All deposits made to this address will be auto-converted to USD and transferred to your account on our ledger.

Deposit outcomes and webhooks

Platforms should be listening to the following webhooks for Accounts Receivable:

When a deposit reaches a Merchant's address, zerohash name matches its Travel Rule information against the Expected Sender, as specified in the account_label. The Travel Rule information comes either from the Travel Rule network or from a whitelisted self-custody wallet.

ConditionOutcomeWebhook
Travel Rule information matches the Expected SenderDeposit is converted and credited to available. Account Funding webhook with status_reason_code = DEPOSIT_PROCESSED and account_label referencing the Third Party Payor's participant code
Travel Rule information does not match the Expected SenderFunds are held and not credited

Account Funding webhook with status_reason_code = TRAVEL_RULE_MATCH_FAILED

Deposit/Withdraw webhook with state = quarantined

No Travel Rule information received Standard Travel Rule collateral flow: a 48-hour remediation window.
  • If the missing information is collected, the name match runs as above.
  • If the missing information is not collected and the 48-hour timer runs out, funds are not converted and are credited to recovery_qurantine for the Expected Sender. The Expected Sender can recover funds from the zerohash europe branded secondary portal.


Find more information on Travel Rule information remediation here.

Account Funding webhook with status_reason_code = TRAVEL_RULE_REVIEW_TIMEOUT

Deposit/Withdraw webhook with state = recovery_quarantine

Travel Rule information matches the Expected Sender, Fund Failure Scenario (ex. deposit below minimum, received asset not enabled)Funds are not converted and are credited to recovery_qurantine. The Third Party Payor can recover funds from the zerohash europe branded secondary portal. Account Funding webhook with status_reason_code varying depending on failure reason

Deposit/Withdraw webhook with state = completed

Create a deposit address for the Merchant

Merchants must also use a deposit address that is unique to them for me-to-me Account Funding deposits. To create a re-usable deposit address for a Merchant, use the POST /fund/rfq endpoint. No account_label needs to be specified for Merchant deposit addresses, and funds will be automatically legered to account_label = general.

Example Request:

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

Notes:

  • See client_fund_id release notes on behavior here.
  • See POST /fund/rfq API reference for possible responses.

Merchant deposits crypto or stablecoins

Merchants can use the deposit_address returned by POST /fund/rfq to facilitate an Account Funding (me-to-me) deposit. The addresses don't expire, so the Merchant can re-use their unique deposit address. Any amount within the standard minimum and maximum thresholds is accepted. All deposits made to this address will be auto-converted to USD and transferred to your account on our ledger.

Deposit outcomes and webhooks

Platforms should be listening to the following webhooks for Accounts Receivable:

When a deposit reaches a Merchant's address, if the account_label is not specified as an Expected Sender, then zerohash name matches its Travel Rule information against the Merchant. The Travel Rule information comes either from the Travel Rule network or from a whitelisted self-custody wallet.

ConditionOutcomeWebhook
Travel Rule information matches the MerchantDeposit is converted and credited to available.Account Funding webhook with status_reason_code = DEPOSIT_PROCESSED and account_label = general
Travel Rule information does not match the MerchantFunds are held and not credited

Account Funding webhook with status_reason_code = TRAVEL_RULE_MATCH_FAILED

Deposit/Withdraw webhook with state = quarantined

No Travel Rule information receivedStandard Travel Rule collateral flow: a 48-hour remediation window.
  • If the missing information is collected, the name match runs as above.
  • If the missing information is not collected and the 48-hour timer runs out, funds are not converted and are credited to recovery_qurantine for the Merchant. The Merchant can recover funds from the zerohash europe branded secondary portal.


Find more information on Travel Rule information remediation here.

Account Funding webhook with status_reason_code = TRAVEL_RULE_REVIEW_TIMEOUT

Deposit/Withdraw webhook with state = recovery_quarantine

Travel Rule information matches the Merchant, Fund Failure Scenario (ex. deposit below minimum, received asset not enabled)Funds are not converted and are credited torecovery_qurantine. The Merchant can recover funds from the zerohash europe branded secondary portal. Account Funding webhook with status_reason_code varying depending on failure reason

Deposit/Withdraw webhook with state = completed

Invoice Auditing & Additional Diligence Scenarios

zerohash audits invoice-based business-to-business transactions every month, after they settle. The audit looks for misuse of the product to exchange controlled or restricted goods and services, including those subject to export or import restrictions, in cross-border trade. When a transaction is selected, the Platform must provide the invoice and, for some triggers, additional diligence information within 15 calendar days. For some Third Party Payors, additional diligence information is always requested automatically as soon as zerohash receives a deposit from them, regardless of sampling.

Reach out to your zerohash representative to learn more about Invoice Auditing and Additional Diligence Scenarios.

Email receipts

If email notifications are on for your Platform, zerohash sends the Merchant an email receipt with the details of the deposit received, including the amount, asset type, sender, and status. The email names the Third Party Payor by legal business name. Receipts go only to the Merchant, never to a Third Party Payor.

Query transactions

You can retroactively query the GET /fund/transactions endpoint to view details of prior deposit events. View the code recipe for additional assistance.

Reconciliation - query the movements endpoint

The platform can query GET /movements in order to view all related ledger movements to a deposit event.

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)

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": "Intermed

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",
            "






Did this page help you?