Payins API Integration Guide

See core product page here.

Definitions

TermDefinition
PlatformThe company that is under contract with zerohash and integrates directly with the zerohash's API
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 step of the Payins API integration

StepNotes
Merchant onboardingOnboard the merchants that sell goods and services through your platform. Only applicable when Platform is not acting as the Merchant (ex. PSPs that acquire merchants)
Shopper onboardingOnboard Shoppers making their first crypto or stablecoin purchases
Initiate Shopper's paymentGenerate deposit instructions for Shoppers
Initiate refundLink external account where Shopper will receive funds and facilitate repayment

Payins prerequisites

Complete these checks before you call POST /pay/rfq.

Pre-conditionIf missing, POST /pay/rfq returns
Ask zerohash to enable this product for you401"Your Platform is not configured to use the Pay product"
Transacting participant has signed the ACCOUNT_FUNDING_PAY agreement401"Participant has not signed the required agreement for Pay"
API key has Pay READ_AND_WRITE permission403"This api key does not have write permission to this endpoint" (GET endpoints return the read-permission variant)

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": "MERCH01"
}

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 the type of good being purchased and each Shopper's transaction amount, overall transaction volume, and residence, we have differing level of KYC requirements. See tables below:


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

See more detail on permitted jurisdictions here.

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

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

}
📘

Signature Collection

For the Payins API Solution, it is critical that you collect consent from the Shopper to zerohash's user agreements and share confirmation that consent was collected through the POST /participants/customers/new endpoint.

5. Query Shopper

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


6. Query Shopper Limits

To retrieve transaction limits for a Shopper's tier, query the GET /pay/limits endpoint. You can use the participant_code filter to retrieve an individual Shopper. 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 zerohash:

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

Request a quote - POST /pay/rfq

Generate quote for the payment amount.

Example Request:

{
  "participant_code": "SHOPP1",
  "pay_asset": "USDC",
  "quoted_total": "100.00",
  "quoted_currency": "USD",
  "account_label": "pay",
  "client_reference_id": "order-12345",
  "merchant_participant_code": "MERCH01" //optional, only use when Platform!= Merchant
}

Interpretation of this request - the Shopper (SHOPP1) is making a $100 payment to the Merchant (MERCH01)using USDC as the payment asset. quoted_currency is an enum with one supported value: USD.

Example Response:

{
  "request_id": "14f8ebb8-7530-4aa4-bef9-9d73d56313f3",
  "participant_code": "SHOPP1",
  "pay_asset": "USDC",
  "rate": "1",
  "quoted_currency": "USD",
  "quoted_total": "100",
  "underlying_quantity": "100",
  "price_expire_ts": null,
  "deposit_address": "0x5f59B625036ccB4f7aD27Ca4Cb896e4452AfFDAF",
  "transaction_id": "550e8400-e29b-41d4-a716-446655440000",
  "client_reference_id": "order-1001",
  "is_static": true
}
FieldTypeNotes
request_idUUID stringEchoed back from Zero Hash for request correlation.
participant_codestringShopper participant code.
pay_assetstringNetwork-qualified asset, such as USDC.SOL, USDC.BASE, or BTC.
ratedecimal stringQuoted rate at the moment of RFQ.
quoted_currencystringEnum: USD.
quoted_totaldecimal stringAmount in quoted_currency.
underlying_quantitydecimal stringAmount in pay_asset.
price_expire_tsISO 8601 UTC string or nullMay be null for stablecoins.
deposit_addressstringBlockchain address on the pay_asset network.
transaction_idUUID stringUse this as the path parameter for POST /pay/{id}/rfq. This is distinct from the settled transaction_id in webhook payloads and GET /pay/transactions rows.
client_reference_idstring, optionalWhatever you sent on the request, echoed back. This is a free-form string.
is_staticbooleanIndicates whether the deposit address is reused or generated per RFQ

End customer initiates on-chain payment

On the Platform's user interface, the Shopper is presented with the deposit_address returned by POST /pay/rfq, and the amount to deposit. They use their Metamask wallet, for example, to trigger a withdrawal into the deposit_address for the amount of the payment.


Payment Successful

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,
  "status_reason_code": "DEPOSIT_PROCESSED",
  "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,
  "status_reason_code": "DEPOSIT_PROCESSED",
  "status_reason": "over payment",
  "reference_id": "04918aef-41d1-4dfb-82b8-2fdc232a8d88",
  "raw_fee_bps": "",
  "deposit_fee_bps": "",
  "raw_fee_notional": "",
  "deposit_fee_notional": ""
}

Payment Unsuccessful

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,
  "status_reason_code": "PROCESSING_FAILED",
  "status_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,
  "status_reason_code": "PROCESSING_FAILED",
  "status_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,
  "status_reason_code": "PROCESSING_FAILED",
  "status_reason": "above max threshold",
  "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,
  "status_reason_code": "PROCESSING_FAILED",
  "status_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,
  "status_reason_code": "DEPOSIT_WINDOW_EXPIRED",
  "status_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,
  "status_reason_code": "PROCESSING_FAILED",
  "status_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 /pay/transactions to retroactively view payment details.

Query parameters

All filters combine with AND. The response shape matches the List payin transactions reference page.

Query paramTypeNotes
participant_codestringRequired for default listing.
pageintegerDefault 1.
page_sizeintegerDefault 50, capped at 50. Passing 100 is silently clamped.
transaction_idstringExact match on the settled transaction ID.
pay_assetstringAsset filter, such as USDC.SOL. Response rows return this value as fund_asset.
successbooleantrue returns only settled-with-success transactions.
client_reference_idstringExact match on the value you sent in the RFQ.
deposit_timestamp_gteinteger, Unix secondsLower-bound inclusive.
deposit_timestamp_gtinteger, Unix secondsLower-bound exclusive.
deposit_timestamp_lteinteger, Unix secondsUpper-bound inclusive.
deposit_timestamp_ltinteger, Unix secondsUpper-bound exclusive.

Sample Response:

    {
      "participant_code": "SHOPP1",
      "fund_asset": "USDC",
      "quoted_currency": "USD",
      "deposit_address": "0xabc123usdcwalletaddress",
      "rate": "1.00",
      "quantity": "100.00",
      "notional": "100.00",
      "fund_timestamp": 1712756400,
      "transaction_id": "111e8400-e29b-41d4-a716-446655440000",
      "account_label": "pay",
      "fund_id": "fund-usdc-1001",
      "success": true,
      "status_reason": "",
      "status_reason_code": "",
      "is_first_deposit": true,
      "raw_fee_bps": "25",
      "actual_fee_bps": "25",
      "raw_fee_notional": "0.25",
      "actual_fee_notional": "0.25",
      "deposit_timestamp": 1712756700,
      "source_address": "0xsenderwallet123",
      "deposited_asset": "USDC",
      "client_reference_id": "order-1001",
      "source": {
        "source_type": "on_chain",
        "integration": "ethereum"
      },
      "deposit_fee_type": "DEPOSIT_FEE_TYPE_FLAT",
      "fee_tier_breakdown": []
    }
  ],
  "page": 1,
  "page_size": 50,
  "total_pages": 1
}

10. Settlement

Withdrawals from Merchant or Platform participant accounts to external bank accounts can be initiated through POST /withdrawals/requests.

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

IMPORTANT: You cannot rely on the deposit’s source address as the refund 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 /convert_withdraw/rfq and POST /convert_withdraw/execute endpoint.

The required flow is to always request and confirm a return address directly from the Shopper before initiating the return.

Please speak to a zerohash sales engineer for guidance if interested in this flow.