Payins Integration Guide

Power your checkout with crypto and stablecoins - end to end integration guide

Introduction

The Pay product allows payment service providers or merchants to offer stablecoins or crypto as a payment option at checkout.


Definitions

TermDefinition
PlatformThe company that is under contract with zerohash and integrates directly with zerohash's APIs or SDKs.
MerchantA business entity (non-natural person) that serves as the customer-facing front-end in the transaction flow. Typically, the Merchant is a customer of the Platform and is where the Shopper initiates their interaction. In some cases, the Merchant and Platform may be the same entity.
ShopperThe natural person that pays for the good or service

How the Integration Works

A high-level summary of each integration step, showing whether it uses the API or SDK.

StepAPI or SDK?Notes
Merchant onboardingAPI
Shopper onboardingAPI
Shopper accepts zerohash t's and c'sSDKThis is the first page on the SDK a first-time Shopper will see
Shopper makes paymentSDKThe SDK will facilitate the asset and network selection and will present the expected crypto/stablecoin quantity, address, and quote timing information
Initiate refundAPI
Initiate returnAPI

1. Submit Merchant

Merchant Information

ℹ️

This section is only applicable if the Platform is not also acting as the Merchant. Typically Payment Service Providers (PSP's) who acquire merchants will need to submit Merchants to zerohash.

Begin by submitting the Merchant via POST /participants/entity/new

{
    "platform_code": "PLAT01",
    "entity_name": "Merchant XYZ",
    "legal_name": "Merchant XYZ 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" 
    "submitter_last_name": "Doe"  
    "submitter_title": "Senior Legal Council"
    "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 merchant indefinitely. We’ll use MERCH1 as the participant_code throughout the examples.

See response for expected shape.

You must submit at least 1 beneficial_owners and 1 control_persons. If these persons do not have an SSN, you must submit an ID 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.

Next, submit the Merchants 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": "MERCH1"
}

State Logic

After a successful POST /participants/entity/new submission, the initial status of the entity will be submitted. In order to transition this to approved, you must then submit all required documents via POST /participants/entity/documents.


2. Query Merchant

You can query already-submitted Merchants via the GET /participants endpoint. If you'd like to query a specific Merchant, use theparticipant_code parameter.


3. Fund Refund Account

To enable future refunds, you must pre-fund your Refund account by sending a wire transfer to the designated bank account provided by our settlement team. Ensure the wire memo is tagged with your Platform code + "_refund" (ie, PLAT01_refund):

Here are the account details:

  • Participant_code: 00SCXM
  • Account_group: PLAT01 (replace with your participant_code)
  • Account_label: pay_refund
  • Account_type: available
  • Asset: USD

4. Submit Shopper

Next, you'll need to onboard the end users (ie, "Shoppers"). Depending on each Shopper's transaction volume and residence, we have differing level of KYC requirements. See table below:


If a Shopper’s total payment volume exceeds $999 within a 24-hour period, you must meet Tier 2 requirements before any further payments can be processed.

To submit a shopper, send a request to the POST /participants/customers/new endpoint.

Shopper - Data Flow

See the below flow diagram to understand the data flow, sequencing, and possible outcomes of submitting a Shopper

Notes:

  • Step 3 - Zero Hash will automatically and immediately screen the shopper
  • Step 5a - Zero Hash's Compliance team will manually review the flagged individual. This can take up to 24 hours. One strategy to move the Shopper to approved yourself is to update the Zero Hash Shopper record with another data point, allowing our sanctions system to comfortably deem them as not sanctioned (ie, the date of birth). Endpoint: PATCH /participants/customers/{participant_code}
  • See participant webhook information here

Tier 1 Submission Example

Example POST /participants/customers/new request:

{
    "first_name": "John",
    "last_name": "Smith",
    "address_one": "1 Main St.",
    "address_two": "Suite 1000",
    "zip": "10014",
    "city": "New York City",
    "jurisdiction_code": "US-NY",
    "phone_number": "123456789",
    "date_of_birth": "1992-01-10",
    "partial": "true",
}

📘

For the below examples, we'll use the sample Shopper participant code of SHOPP1


5. Query Shopper

To retrieve general information about the shopper, query the GET /participants endpoint. You can also use the participant_code filter to retrieve an individual Shopper.


6. Query Shopper Limits

To check a Shopper’s accumulated volume over the past 24 hours, query the GET /participant/{participant_code}/limits endpoint. Example response:

{
    "participant_code": "SHOPP1",
    "limits": [{
            "type": [
                "payment"
            ],
            "period": 24,
            "balance":"100"
            "limit": "999",
            "asset": "USD"
      }
    ]
}

Interpretation of the above: over the last 24 hours, the Shopper has done $100 of payment activity.


7. Accounts

The Platform will have the following accounts at Zero Hash:

AccountDescriptionTechnical Details
Settlement AccountThe fiat account that contains the balance of successful payments- participant_code = [your platform code] - account_group = [your platform code] - account_label = general - account_type = available - asset = USD (or other fiat currency being used)
Refund Float AccountThe fiat account, funded by the Platform, that is used to refund Shoppers. The balances here will be converted to a crypto or stablecoin asset and sent on-chain to the Shopper. See section, "11. Refund a Payment" below for details- participant_code = 00SCXM - account_group = [your platform code] - account_label = pay_refund - account_type = available - asset = USD (or other fiat currency being used)
Error AccountThe fiat account that accumulates due to error scenarios (over payment, under payment, incorrect asset or network, late payment- participant_code = [your platform code] - account_group = [your platform code] - account_label = pay_error - account_type = available - asset = USD (or other fiat currency being used)

8. Initiate Payment

Now, let's move onto money movement. Begin by requesting an access token specifying:

FieldDescriptionExample ValueRequired?
participant_codeThe 6-digit alpha numeric code associated with the Customer that is looking to withdraw (the response of the original POST /participants/customers/new call)SHOPP1Y
permissionsThe array of permissions that will be granted to the returned JWT token["crypto-pay"]Y
purchase_amount
(part of the payment_details object)
The amount of the good or service being paid for, denominated in the denominated_currency100Y
denominated_currency
(part of the payment_details object)
The fiat or crypto currency in which the purchase amount is pricedUSDY
reference_idPlatform-dictated reference id that is tied to any successful payment0bd7f7f0-cf26-495f-b2df-e8afe8481ba3N

Example POST /client_auth_token request:

{
 "participant_code": "SHOPP1",
 "permissions": ["crypto-pay"],
 "payment_details" : {
      "purchase_amount": "100",
      "denominated_currency" : "USD"
   },
 "reference_id": "0bd7f7f0-cf26-495f-b2df-e8afe8481ba3"
}

After successfully calling POST /client_auth_token, the next step is to initiate the Pay SDK flow. See the example below for how to implement this in a React application. Keep in mind that if you're using a Native Mobile App (Swift, Flutter, etc) instead of using zh-web-sdk you should follow the WebView approach, described here.

import React from 'react';
import ZeroHashSDK, { AppIdentifier } from 'zh-web-sdk';

const App = () => {
  const sdk = new ZeroHashSDK({
    zeroHashAppsURL: "https://web-sdk.cert.zerohash.com",
    PAYJWT: "<JWT_TOKEN_HERE>" 
  });

    sdk.openModal({
    appIdentifier: AppIdentifier.PAY, 
  })
  return <></>;
}

export default App;

8a. Accept T's and C's

First-time Shoppers will need to accept the zerohash terms and conditions:

8b. Select asset and network

The Shopper will now select the asset they want to pay with. Remember, the assets that are displayed on this screen are your choice - contact your zerohash contact to make the proper configurations:

Depending on the selected asset, the next screen may prompt the Shopper to choose a network. If the asset only supports a single network - for example, Bitcoin (BTC) only supports the Bitcoin network for this product currently - the Select Network screen is skipped.

In the example below, USDC was selected as the asset, which supports multiple networks. As a result, the next screen displays all available network options for the Shopper to choose from.


8c. Payment pending

The Shopper is now presented with the information to successfully make the payment:

  • Conversion rate
  • Payment amount
  • Payment expiration time
  • QR code and address

The expectation is that the Shopper, with relative urgency, takes the following steps:

If the Shopper is paying from their mobile phone

  1. Copies address
  2. Navigates to their preferred exchange or wallet (ie, Coinbase Exchange or Metamask wallet)
  3. Selects the same Asset they chose on the zerohash SDK
  4. Selects the same Network they chose on the zerohash SDK
  5. Enters the exact amount that was displayed on the Payment Pending screen
  6. Initiate the transaction

If the Shopper is paying from their computer (web)

  1. Navigates to their preferred exchange or wallet (ie, Coinbase Exchange or Metamask wallet)
  2. Selects the same Asset they chose on the zerohash SDK
  3. When asked to enter the address, selects the "scan QR code" option
  4. Point your device’s camera at the QR code displayed on your computer screen, ensuring it’s centered and clearly visible
  5. Selects the same Network they chose on the zerohash SDK
  6. Enters the exact amount that was displayed on the Payment Pending screen
  7. Initiate the transaction

8d. Payment Successful

If the Shopper sends at least the displayed amount of the correct crypto or stablecoin, using the specified asset and network, the SDK will automatically transition to the next screen:

Successful Payment (exact amount)

Once a successful payment is received for the exact amount, we'll send a webhook to you that looks like this:

{
  "participant_code": "SHOPP1",
  "fund_asset": "USDC.BASE",
  "rate": "1",
  "quoted_currency": "USD",
  "source_address": "external",
  "deposit_address": "0xb07C48f0BF25A97034c818Dc643A98e42703D20c",
  "quantity": "100",
  "notional": "100",
  "fund_id": "6767a149-4771-4166-ba07-a2542e361318",
  "fund_timestamp": 1756376889792000000,
  "deposit_timestamp": 1756376889499000000,
  "transaction_id": "d1d731ed-952c-410e-b62b-eb00e3d096fc",
  "account_label": "general",
  "success": true,
  "reason": "",
  "reference_id": "04918aef-41d1-4dfb-82b8-2fdc232a8d88",
  "raw_fee_bps": "",
  "deposit_fee_bps": "",
  "raw_fee_notional": "",
  "deposit_fee_notional": ""
}

Successful Payment (over payment)

Once a successful payment is received where the Shopper sent too much, we'll send a webhook to you that looks like this:

{
  "participant_code": "SHOPP1",
  "fund_asset": "USDC.BASE",
  "rate": "1",
  "quoted_currency": "USD",
  "source_address": "external",
  "deposit_address": "0xb07C48f0BF25A97034c818Dc643A98e42703D20c",
  "quantity": "100",
  "notional": "100",
  "fund_id": "6767a149-4771-4166-ba07-a2542e361318",
  "fund_timestamp": 1756376889792000000,
  "deposit_timestamp": 1756376889499000000,
  "transaction_id": "d1d731ed-952c-410e-b62b-eb00e3d096fc",
  "account_label": "general",
  "success": true,
  "reason": "over payment",
  "reference_id": "04918aef-41d1-4dfb-82b8-2fdc232a8d88",
  "raw_fee_bps": "",
  "deposit_fee_bps": "",
  "raw_fee_notional": "",
  "deposit_fee_notional": ""
}

8d. Payment Unsuccessful

There are a few scenarios that will lead a failed payment which will be reflected on the SDK UI:


Failure scenarioExpected remediation steps
Under paymentShopper to contact Platform → Platform to facilitate a withdraw via POST /payments to Shopper's wallet
DepegShopper to contact Platform → Platform to contact zerohash via Slack or Email to facilitate withdrawals back to the Shopper manually
Above max thresholdShopper to contact Platform → Platform to contact zerohash via Slack or Email to facilitate withdrawals back to the Shopper manually
QuarantineShopper to contact Platform → Platform to contact zerohash via Slack or Email. Note: zerohash will likely disable this Shopper, as this scenario is typically triggered due receiving a deposit from a high-risk source address
Late paymentShopper to contact Platform → Platform to facilitate a withdraw via POST /payments to Shopper's wallet
Wrong asset or networkShopper to contact Platform → Platform to facilitate a withdraw via POST /payments to Shopper's wallet

Payment Unsuccessful (under payment)

Upon a payment for an insufficient amount, a webhook notification will be triggered as follows

{
  "participant_code": "SHOPP1",
  "fund_asset": "USDC.BASE",
  "rate": "1",
  "quoted_currency": "USD",
  "source_address": "external",
  "deposit_address": "0xb07C48f0BF25A97034c818Dc643A98e42703D20c",
  "quantity": "90",
  "notional": "",
  "fund_id": "6767a149-4771-4166-ba07-a2542e361318",
  "fund_timestamp": 1756376889792000000,
  "deposit_timestamp": 1756376889499000000,
  "transaction_id": "d1d731ed-952c-410e-b62b-eb00e3d096fc",
  "account_label": "general",
  "success": false,
  "reason": "under payment",
  "reference_id": "04918aef-41d1-4dfb-82b8-2fdc232a8d88",
  "raw_fee_bps": "",
  "deposit_fee_bps": "",
  "raw_fee_notional": "",
  "deposit_fee_notional": ""
}

Successful Payment (depeg)

If a stablecoin becomes depegged, to mitigate zerohahs's risk, we will manually mark the asset as depegged on our side. This will block all “sell” transactions converting that stablecoin to USD. Should we receive a payment deposit during this period, we will not convert the deposit to fiat. Instead, the funds will be moved to a dedicated depeg account, and a webhook notification will be triggered as follows:

{
  "participant_code": "SHOPP1",
  "fund_asset": "USDC.BASE",
  "rate": "1",
  "quoted_currency": "USD",
  "source_address": "external",
  "deposit_address": "0xb07C48f0BF25A97034c818Dc643A98e42703D20c",
  "quantity": "100",
  "notional": "",
  "fund_id": "6767a149-4771-4166-ba07-a2542e361318",
  "fund_timestamp": 1756376889792000000,
  "deposit_timestamp": 1756376889499000000,
  "transaction_id": "d1d731ed-952c-410e-b62b-eb00e3d096fc",
  "account_label": "general",
  "success": false,
  "reason": "depeg",
  "reference_id": "04918aef-41d1-4dfb-82b8-2fdc232a8d88",
  "raw_fee_bps": "",
  "deposit_fee_bps": "",
  "raw_fee_notional": "",
  "deposit_fee_notional": ""
}

Above max threshold

zerohash has certain liquidation maximums on our end. If a large deposit is received above this threshold, a webhook notification will be triggered as follows:

{
  "participant_code": "SHOPP1",
  "fund_asset": "USDC.BASE",
  "rate": "1",
  "quoted_currency": "USD",
  "source_address": "external",
  "deposit_address": "0xb07C48f0BF25A97034c818Dc643A98e42703D20c",
  "quantity": "10000000",
  "notional": "",
  "fund_id": "6767a149-4771-4166-ba07-a2542e361318",
  "fund_timestamp": 1756376889792000000,
  "deposit_timestamp": 1756376889499000000,
  "transaction_id": "d1d731ed-952c-410e-b62b-eb00e3d096fc",
  "account_label": "general",
  "success": false,
  "reason": "depeg",
  "reference_id": "04918aef-41d1-4dfb-82b8-2fdc232a8d88",
  "raw_fee_bps": "",
  "deposit_fee_bps": "",
  "raw_fee_notional": "",
  "deposit_fee_notional": ""
}

Quarantine

If zerohash detects that the source address of the payment deposit is high-risk, a webhook notification will be triggered as follows:

{
  "participant_code": "SHOPP1",
  "fund_asset": "USDC.BASE",
  "rate": "1",
  "quoted_currency": "USD",
  "source_address": "external",
  "deposit_address": "0xb07C48f0BF25A97034c818Dc643A98e42703D20c",
  "quantity": "10000000",
  "notional": "",
  "fund_id": "6767a149-4771-4166-ba07-a2542e361318",
  "fund_timestamp": 1756376889792000000,
  "deposit_timestamp": 1756376889499000000,
  "transaction_id": "d1d731ed-952c-410e-b62b-eb00e3d096fc",
  "account_label": "general",
  "success": false,
  "reason": "For Zero Hash Compliance reasons, the deposit has not been converted to fiat and the crypto has been credited to a Zero Hash holding account",
  "reference_id": "04918aef-41d1-4dfb-82b8-2fdc232a8d88",
  "raw_fee_bps": "",
  "deposit_fee_bps": "",
  "raw_fee_notional": "",
  "deposit_fee_notional": ""
}

Late payment

If zerohash receives a payment deposit past the Payment Window, a webhook notification will be triggered as follows:

{
  "participant_code": "SHOPP1",
  "fund_asset": "USDC.BASE",
  "rate": "1",
  "quoted_currency": "USD",
  "source_address": "external",
  "deposit_address": "0xb07C48f0BF25A97034c818Dc643A98e42703D20c",
  "quantity": "10000000",
  "notional": "",
  "fund_id": "6767a149-4771-4166-ba07-a2542e361318",
  "fund_timestamp": 1756376889792000000,
  "deposit_timestamp": 1756376889499000000,
  "transaction_id": "d1d731ed-952c-410e-b62b-eb00e3d096fc",
  "account_label": "general",
  "success": false,
  "reason": "payment session has expired",
  "reference_id": "04918aef-41d1-4dfb-82b8-2fdc232a8d88",
  "raw_fee_bps": "",
  "deposit_fee_bps": "",
  "raw_fee_notional": "",
  "deposit_fee_notional": ""
}

Wrong asset or network

If zerohash receives a payment deposit for an asset that was not selected by the Shopper on the Select Asset page, a webhook notification will be triggered as follows:

{
  "participant_code": "SHOPP1",
  "fund_asset": "ETH",
  "rate": "1",
  "quoted_currency": "USD",
  "source_address": "external",
  "deposit_address": "0xb07C48f0BF25A97034c818Dc643A98e42703D20c",
  "quantity": ".15",
  "notional": "",
  "fund_id": "6767a149-4771-4166-ba07-a2542e361318",
  "fund_timestamp": 1756376889792000000,
  "deposit_timestamp": 1756376889499000000,
  "transaction_id": "d1d731ed-952c-410e-b62b-eb00e3d096fc",
  "account_label": "general",
  "success": false,
  "reason": "deposit currency does not match the expected currency",
  "reference_id": "04918aef-41d1-4dfb-82b8-2fdc232a8d88",
  "raw_fee_bps": "",
  "deposit_fee_bps": "",
  "raw_fee_notional": "",
  "deposit_fee_notional": ""
}

9. Query payments

You can query our REST endpoint GET /fund/transactions to retroactively view payment details.


10. Settlement

Zero Hash will, one time per day, send you a fiat settlement wire where the amount represents the sum of all converted payments 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)


11. Refund a Payment

In order to initiate a refund, you should use the following endpoints:

  • POST /payments/external_accounts - links a crypto account where the Shopper will receive their refund
  • POST /payments - initiates a conversion from USD (using Refund account) to the crypto asset. This API call will also automatically and immediately send the funds on-chain.