Remittance Integration Guide

Definitions

TermDefinitionExample
PlatformThe company that's in contract with Zero Hash and directly interacts with Zero Hash's API's or SDK's.Remittance Co. LLC
SenderThe initiator of the Remittance (a fully-onboarded natural person subject to Sender Tiers).John Smith
Sender CurrencyThe currency that the Sender is using to fund the Remittance.USD
Remittance TokenThe stablecoin or crypto asset being used to move value cross-border.USDC
Remittance ConversionThe conversion of the Sender Currency to stablecoin or crypto.A buy of USDC for USD (symbol: USDC/USD)
BeneficiaryThe receiver of the Remittance (not onboarded, but diligence is required).Hector Garcias
Beneficiary CurrencyThe currency that the Beneficiary is using to fund the Remittance.MXN

High Level Flow

  1. Fund Fiat Accounts
  2. Submit Sender
  3. Query Sender Limits
  4. Submit Beneficiary
  5. Query Beneficiary
  6. Update Beneficiary
  7. Initiate Conversion
  8. Initiate Transfer
  9. Initiate Withdrawal
  10. Query Withdrawal
  11. End of Day Settlement

Initial Setup

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

1. Fund Fiat Accounts

There are 2 types of fiat accounts needed for this flow: Float account and Settlement Reserve account:

  • Float account: The account that will fund real-time remittance transactions
  • Settlement reserve account: The account that will be used to top-up the settlement account, once a day

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

The Platform will fund each of the above accounts by sending fiat to the proper bank account, tagging each wire with a memo equal to the Platform's participant_code. Here are the account details:

Float AccountSettlement Reserve Account
Participant code00SCXM[Platform's participant code]
Account group[Platform's participant code][Platform's participant code]
Account labelgeneralsettlement_reserve
Account typeAvailableAvailable
AssetUSDUSD

2. Submit Sender

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.

Sender Tiers

We've established a tiering system for all Senders. Depending on their volume over a certain time period, Senders will have different onboarding requirements, which the Platform will be responsible for collecting and relaying to Zero Hash:

Interpretation:

  • 24 hours: A Sender can remit up to $999 over a 24 hour period. If the Sender wants remit $1,000 or higher over a 24 hour period, Zero Hash must collect Tier 2 data first. If the Sender wants remit $3,000 or higher over a 24 hour period, Zero Hash must collect Tier 3 data first. A Sender cannot remit more than $10,000 over a 24 hour period.
  • 30 days: A Sender can remit up to $3,000 over a 30 day period. If the Sender wants remit $3,001 or higher over a 30 day period, Zero Hash must collect Tier 2 data first. If the Sender wants remit $10,000 or higher over a 30 day period, Zero Hash must collect Tier 3 data first. A Sender cannot remit more than $20,000 over a 30 day period.
  • 180 days: A Sender can remit up to $30,000 over a 180 day period. If the Sender wants remit $30,001 or higher over a 180 day period, Zero Hash must collect Tier 3 data first. A Sender cannot remit more than $60,000 over a 180 day period.
  • Lifetime: If a Sender wants to remit $9,001 or higher over their lifetime, Zero Hash must collect Tier 2 data first.

Sender - Submit via API

The Platform submits the Sender via POST /participants/customers/new.

POST /participants/customers/new Tier 1 example request:

{
    "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,
    "signed_agreements": [{
        "type": "user_agreement",
        "region": "us",
        "signed_timestamp": 1603378501286
    }],
}

POST /participants/customers/new Tier 2 example request:

{
    "first_name": "John",
    "last_name": "Smith",
    "email": "[email protected]",
    "phone_number": "+12345838374",
    "date_of_birth": "1985-09-02",
    "ip_address": "66.249.73.192",
    "address_one": "1 Main St.",
    "address_two": "Suite 1000",
    "id_number": "123456789",
    "id_number_type": "ssn",
    "liveness_check": "pass",
    "idv": "pass",
    "city": "Chicago",
    "zip": "12345",
    "jurisdiction_code": "US-IL",
    "partial": true,
    "signed_agreements": [
        {
            "type": "user_agreement",
            "region": "us",
            "signed_timestamp": 1730906918901
        }
    ]
}

POST /participants/customers/new Tier 3 example request:

{
    "first_name": "John",
    "last_name": "Smith",
    "email": "[email protected]",
    "phone_number": "+12345832133",
    "date_of_birth": "1985-09-02",
    "ip_address": "66.249.73.192",
    "address_one": "1 Main St.",
    "address_two": "Suite 1000",
    "id_number": "123456789",
    "id_number_type": "ssn",
    "liveness_check": "pass",
    "idv": "pass",
    "city": "Chicago",
    "zip": "12345",
    "jurisdiction_code": "US-IL",
    "source_of_funds": "salary",
    "employment_status": "full_time",
    "industry": "food_beverages",
    "partial": true,
    "signed_agreements": [
        {
            "type": "user_agreement",
            "region": "us",
            "signed_timestamp": 1730907129144
        }
    ]
}

Note: The Sender will be agreeing to the Zero Hash terms and conditions. The platform indicates to Zero Hash 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 Remittances, the Sender is signing the standard user agreement, so a value of user_agreement is expected.
  • signed_timestamp: is the timestamp that the Sender accepted the terms and conditions.
  • region: represents the Zero Hash entity that the Sender is entering into a contract with. The only acceptable value right now is US.

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": "SENDR1",
     "tax_id": "",
     "citizenship": "",
     "citizenship_code": "",
     "kyc": "unknown",
     "kyc_timestamp": null,
     "onboarded_location": "US-IL",
     "sanction_screening": "unknown",
     "sanction_screening_timestamp": null,
     "idv": "unknown",
     "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 Sender. This uniquely identifies the Sender indefinitely and should be use in subsequent API calls where applicable. We’ll use SENDR1 as the participant_code throughout the examples.

3. Query Sender Limits

A Platform can query the API to understand the Sender's remaining limits via the GET /participant/[participant_code]/limits endpoint.

GET /participant/SENDR1/limits response:

{
    "participant_code": "SENDR1",
    "limits": [{
            "type": [
                "trades"
            ],
            "period": 24,
            "limit": "999",
            "asset": "USD"
        },
        {
            "type": [
                "trades"
            ],
            "period": 720,
            "limit": "3000",
            "asset": "USD"
        },
        {
            "type": [
                "trades"
            ],
            "period": 0,
            "limit": "9000",
            "asset": "USD"
        },
        {
            "type": [
                "trades"
            ],
            "period": 4320,
            "limit": "0",
            "asset": "USD"
        }
    ]
}

Interpretation:

  • The Sender (SENDR1) can remit a maximum of 999 in notional remittance volume over the next 24 hours. If the Sender wants to remit any more, Zero Hash is required to collect Tier 2 data.
  • The Sender can remit a maximum of 3,000 in notional remittance volume over the next 720 hours (30 days). If the Sender wants to remit any more, Zero Hash is required to collect Tier 2 data.
  • The Sender can remit a maximum of 9,000 in notional remittance volume over its lifetime. If the Sender wants to remit any more, Zero Hash is required to collect Tier 2 data.
  • The Sender does not have a limit for the period of 4320 hours (180 days) as this does not apply to Tier 1.

Note:

  • The limit represents the notional remittance volume that is permitted over the next period seconds. When a remittance transaction becomes older than period, that transaction value is refunded back into the limit balance.
  • When the limit becomes 0 or a transaction would take the limit into a negative value, Zero Hash will reject subsequent API calls to initiate any remittances (POST /liquidity/rfq and POST /liquidity/execute).
  • A period of 0 means a lifetime limit.
  • The period unit is hours

4. Submit Beneficiary

The Platform will need to submit a Beneficiary via POST /participants/beneficiaries/new:

{
    "first_name": "Hector",
    "last_name": "Garcias",
    "address_one": "1 Main St.", // optional
    "address_two": "Suite 1000", // optional
    "jurisdiction_code": "MX-CMX",
    "date_of_birth": "1985-09-02", // optional
}

Note:

  • The Beneficiary is not agreeing to any Zero Hash terms and conditions, so the signed_timestamp array is not required.
  • date_of_birth, address_1, and address_2 are optional felids.
  • jurisdiction_codefollows the ISO 3166-2 standard.

POST /participants/beneficiaries/new response:

{
    "participant_code": "BENEF1",
    "created_at": 1603378501789,
    "first_name": "Hector",
    "last_name": "Garcias",
    "jurisdiction_code": "MX-CMX",
    "date_of_birth": "1985-09-02"
}

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

Beneficiary State Logic

After a successful POST /participants/beneficiaries/new submission, the initial status of the Beneficiary will be submitted. At this point, Zero Hash is running an automated compliance screening. If the person passes this check, the status will transition to an approved state. If the compliance screening results in a hit, the status will transition to a pending_approval status. Note: there is also a scenario where the Beneficiary transitions directly into a rejected state, depending on the compliance score. Zero Hash’s compliance team will become alerted and will manually review the Beneficiary within 24 hours. If the determination after that review is that the Beneficiary should not have been flagged, the status will transition to approved. Otherwise, the status will transition to rejected.

5. Query Beneficiary

Platforms can query already-submitted Beneficiaries via the GET /participants endpoint. A common query parameter is the participant_code, however the full list can be found on the API reference.

GET /participants?participant_code=BENEF1 response:

{
    "participant_code": "477S50",
    "participant_name": "Hector Garcias",
    "email": "[email protected]",
    "status": "pending_approval",
    "reason_code": null,
    "country": "Mexico",
    "state": "CMX",
    "jurisdiction_code": "MX-CMX",
    "updated_at": 1730375356473,
    "lifetime_remaining_limit": "0",
    "daily_remaining_limit": "0",
    "limit_currency": "USD",
    "limits": [{
            "currency": "USD",
            "types": [
                "trades"
            ],
            "period": 24,
            "balance": "0",
            "remaining_limit": "0"
        },
        {
            "currency": "USD",
            "types": [
                "trades"
            ],
            "period": 720,
            "balance": "0",
            "remaining_limit": "0"
        },
        {
            "currency": "USD",
            "types": [
                "trades"
            ],
            "period": 0,
            "balance": "0",
            "remaining_limit": "0"
        },
        {
            "currency": "USD",
            "types": [
                "trades"
            ],
            "period": 4320,
            "balance": "0",
            "remaining_limit": "0"
        }
    ]
}

6. Update Beneficiary

The Platform has the ability to update any already-submitted Beneficiary record using the PATCH /participants/beneficiaries/{participant_code} endpoint.

{
    "address_one": "1 Main St.",
    "address_two": "Suite 10001",
    "date_of_birth": "1985-09-02",
    "platform_updated_at": 1730113960212
}

Note that if the Platform didn't originally submit the optional fields (date_of_birth, address_1, or address_2) on the POST /participants/beneficiaries/new endpoint, they can use the Update Beneficiary endpoint to attach these values onto a Beneficiary record. These additional fields may allow Beneficiaries to transition from a pending_approval status to approved as the additional information may allow the sanction screening checks to pass.

7. Initiate Conversion

The Platform will now convert the Sender Currency to the Remittance Token using the POST /liquidity/rfq and POST /liquidity/execute endpoint.

POST /liquidity/rfq request:

{
  "participant_code":"SENDR1",
  "side": "buy",
  "underlying": "USDC",
  "quoted_currency": "USD",
  "total": "500", -- the total amount being charged to the Sender for the remittance
  "fees": [
    {
      "name": "processing_fee", -- free form field to describe the fee type
      "amount": "2.00" -- represents the fee assessed to the customer at the time of payment
    }
  ],
  "payment_processor": {         
      "name": "checkout.com", -- the name of the PSP clearing the fiat leg of the trade
      "id": "5cd07738b861c31e3bd61467BTC1Buy1568311644602" -- the Platform-determined unique ID associated with the remittance - this will be used by Zero Hash to reconcile with the PSP
    }
}

Note:

  • The Sender is used for the participant_code
  • There is a credit check against the Float Account. The balance on the account must be at least the amount of the submitted total
  • The Remittance Token is specified via the underlying field
  • The Sender Currency is specified via the quoted_currency field
  • The remittance amount (in Sender Currency terms) is specified via the total field

POST /liquidity/rfq response:

{
  "request_id": "ce819fe8-b1d7-43bb-961c-e09ede0988d3",
  "participant_code": "PLAT01",
  "quoted_currency": "USD",
  "side": "BUY",
  "quantity": "500",
  "price": "1.00",
  "quote_id": "32ad471b-824e-4f04-94ff-45c4439b4fe9",
  "expire_ts": 1568311649602,
  "account_group": "PLAT01",
  "account_label": "general",
  "obo_participant": {
    "participant_code": "SENDR1",
    "account_group": "PLAT01",
    "account_label": "general"
  },
  "total_notional": "500.00",
  "underlying": "USDC",
  "asset_cost_notional": "500.00",
  "spread_notional": "0",
  "spread_bps": "0",
}

Note:

  • The quote_id will be used in the next call to execute the quote. The quote is valid for a configurable length of time. 5 or 30 seconds is recommended for Remittances.

POST /liquidity/execute request:

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

Note:

  • This call executes the quote initiates the USDC/USD trade
  • Here are the ledger movements that take place after a successful execution:
Movement TypeParticipantAssetType
TransferPlatform (Float Account)USDDebit
TransferSenderUSDCredit
Final SettlementEnd CustomerUSDCCredit

8. Initiate Transfer

The Platform will then initiate a transfer (this is just an internal transfer on the Zero Hash ledger) via the POST /transfers endpoint:

POST /transfers request:

{
    "from_participant_code":"SENDR1",
    "from_account_group":"PLAT01",
    "to_participant_code":"PLAT01",
    "to_account_group":"PLAT01",
    "asset":"USDC",
    "amount":"500"
}

Note: the transfer will settle instantly to the Platform's USDC account.

9. Initiate Withdrawal

The Platform will then withdraw the Remittance Token on-chain via the POST /withdrawals/requests endpoint.

POST /withdrawals/requests request:

{ 
   "address":"0xcDA0D6adCD0f1CCeA6795F9b1F23a27ae643FE7C",
   "participant_code":"PLAT01",
   "amount":"500",
   "asset":"USDC",
   "account_group":"PLAT01",
   "sender_participant_code": "SENDR1",
   "beneficiary_participant_code": "BENEF1",
}

Note:

  • This request moves the Remittance Token on-chain to the provided address
  • The sender_participant_code and beneficiary_participant_code are needed in order for Zero Hash to be Travel Rule-compliant
  • The network fee associated with the transfer is incurred by the Platform

POST /withdrawals/requests response:

{
    "message": {
        "id": "1be57e68-80cf-4994-ad2b-c305965c9115",
        "withdrawal_account_id": 342298,
        "participant_code": "PLAT01",
        "account_group": "PLAT01",
        "account_label": null,
        "requestor_participant_code": "PLAT01",
        "asset": "USDC.ETH",
        "requested_amount": "500",
        "settled_amount": null,
        "gas_price": null,
        "status": "APPROVED",
        "on_chain_status": null,
        "client_withdrawal_request_id": null,
        "requested_timestamp": 1726090966370,
        "transaction_id": null,
        "input_data": null,
        "fee_amount": null,
        "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,
        "amount_in_usd": 500,
        "sender_participant_code": "SENDR1",
        "beneficiary_participant_code": "BENEF1",
    }
}

Note

  • The withdrawal will initially be in an APPROVED status
    • See guide here for explanation on the status and on_chain_status fields

10. Query Withdrawal

The Platform can retrieve information related to all withdrawals via the GET /withdrawals/requests endpoint.

Additionally, Platforms can check an individual withdrawal via GET /withdrawals/requests/id where the id is the withdrawal_request_id.

GET /withdrawals/requests/id response. In this example, the withdrawal is fully settled on-chain:

{
    "message": [
        {
            "id": "1be57e68-80cf-4994-ad2b-c305965c9115",
            "withdrawal_account_id": 342298,
            "participant_code": "PLAT01",
            "account_group": "PLAT01",
            "account_label": null,
            "requestor_participant_code": "PLAT01",
            "asset": "USDC.ETH",
            "requested_amount": "500",
            "settled_amount":"500",
            "gas_price": null,
            "status": "SETTLED",
            "on_chain_status": "CONFIRMED",
            "client_withdrawal_request_id": null,
            "requested_timestamp": 1726090966370,
            "transaction_id": "0xe0875f35e9e526d60bfc57f0df92d308ab44a34d7dd5c06f100c0dd07def7125",
            "input_data": null,
            "fee_amount": null,
            "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,
            "sender_participant_code": "SENDR1",
            "beneficiary_participant_code": "BENEF1",
        }
    ]
}

11. Transitioning from Tier 1 to Tier 2

In order to transition a Sender from Tier 1 to Tier 2, and allow them to remit

  • more than $999 over a 24 hour period, or
  • more than $3,000 over a 30 day period, or
  • more than $9,000 over the Sender's lifetime

The Platform must do the following:

  1. Submit a physical document via the POST /participants/documents endpoint

Example POST /participants/documents request:

{
    "document": "{{base 64 encoded file}}",
    "mime": "pdf",
    "file_name": "john_smith_passport.pdf",
    "participant_code": "SENDR1"
}
  1. Update the existing Sender participant via the PATCH /participants/customers/{participant_code} endpoint, changing the following fields:
  • id_number
  • id_number_type
  • liveness_check
  • tax_id (this will be the SSN value if the Sender actually possesses an SSN - if not, this can be omitted)

Example PATCH /participants/customers/{participant_code} request:

{
  "id_number": "L12345678",
  "id_number_type": "us_passport",
  "liveness_check": "pass"
  "tax_id": "123456789"   
}

Example where the Sender does not have an SSN:

{
  "id_number": "L12345678",
  "id_number_type": "us_drivers_license",
  "liveness_check": "pass" 
}

12. Transitioning from Tier 2 to Tier 3

In order to transition a Sender from Tier 2 to Tier 3, and allow them to remit

  • more than $2,999 over a 24 hour period, or
  • more than $9,999 over a 30 day period, or
  • more than $60,000 over a 180 day period

The Platform must do the following:

  1. Update the existing Sender participant via the PATCH /participants/customers/{participant_code} endpoint, changing the following fields:
    • source_of_funds
    • industry
    • employment_status

13. End of Day Settlement

Platform's will typically use a Payment Service Provider (PSP) to process the USD leg of the transaction. PSP's will be required to settlement with Zero Hash on a daily basis (excluding weekends and holidays). PSP's will typically withhold both 1) chargebacks and 2) PSP fees. As a result, Zero Hash will receive a settlement amount that is less than the Net Delivery Obligation. To compensate for this, Zero Hash will sweep funds from the Settlement Reserve Account to the Settlement Account, completing end of day settlement.