Remittance Integration Guide
Definitions
Term | Definition | Example |
---|---|---|
Platform | The company that's in contract with Zero Hash and directly interacts with Zero Hash's API's or SDK's. | Remittance Co. LLC |
Sender | The initiator of the Remittance (a fully-onboarded natural person subject to Sender Tiers). | John Smith |
Sender Currency | The currency that the Sender is using to fund the Remittance. | USD |
Remittance Token | The stablecoin or crypto asset being used to move value cross-border. | USDC |
Remittance Conversion | The conversion of the Sender Currency to stablecoin or crypto. | A buy of USDC for USD (symbol: USDC/USD) |
Beneficiary | The receiver of the Remittance (not onboarded, but diligence is required). | Hector Garcias |
Beneficiary Currency | The currency that the Beneficiary is using to fund the Remittance. | MXN |
High Level Flow
- Fund Fiat Accounts
- Submit Sender
- Query Sender Limits
- Submit Beneficiary
- Query Beneficiary
- Update Beneficiary
- Initiate Conversion
- Initiate Transfer
- Initiate Withdrawal
- Query Withdrawal
- 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 Account | Settlement Reserve Account | |
---|---|---|
Participant code | 00SCXM | [Platform's participant code] |
Account group | [Platform's participant code] | [Platform's participant code] |
Account label | general | settlement_reserve |
Account type | Available | Available |
Asset | USD | USD |
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 ofuser_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 isUS
.
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 nextperiod
seconds. When a remittance transaction becomes older thanperiod
, that transaction value is refunded back into thelimit
balance. - When the
limit
becomes 0 or a transaction would take thelimit
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 lifetimelimit
. - 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
, andaddress_2
are optional felids.jurisdiction_code
follows 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 Type | Participant | Asset | Type |
---|---|---|---|
Transfer | Platform (Float Account) | USD | Debit |
Transfer | Sender | USD | Credit |
Final Settlement | End Customer | USDC | Credit |
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
andbeneficiary_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
andon_chain_status
fields
- See guide here for explanation on the
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:
- 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"
}
- 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:
- 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.
Updated 1 day ago