Skip to main content

Introduction

Welcome to the HIFI Quickstart Guide (USD)! In this guide, we will walk you through the process of creating a user, passing KYC to unlock USD rail, creating a virtual account, adding offramp accounts, simulating onramps in the sandbox, and offramps. By following along, you’ll gain a clear understanding of how our endpoints work and how to integrate them into your application. To get started, you’ll need an API key, which you can get by reaching out to a member of the sales team. You’ll have two different API keys for two different HIFI environments. Today we’ll start in the Sandbox environment with the sandbox API key. You can follow our Sandbox guide to generate a sandbox API key. Let’s first create a user!

User

A user object can represent either an individual or a business. All the available rails, accounts, onramps, offramps, transfer, etc, are associated with a user object. To create a HIFI user, you need to provide a basic set of user information. The user must also review and accept HIFI’s Terms and Service to obtain a valid signed agreement ID, which signifies a legally binding agreement to use our service. A successfully created user will have provisioned wallets and be granted access to our on-chain functionalities.

Get a Valid Signed Agreement ID

To get HIFI’s Terms and Service page, you can call the Generate Terms of Service Link endpoint. You will need to pass in an idempotencyKey, which can be any UUID. This idempotencyKey will be used as your signed agreement ID. Request:
curl --request POST \
     --url https://sandbox.hifibridge.com/v2/tos-link \
     --header 'accept: application/json' \
     --header 'authorization: Bearer zpka_123456' \
     --header 'content-type: application/json' \
     --data '
{
  "idempotencyKey": "8cb49537-bcf9-41b1-8d8c-c9c200d7341b"
}
'
Response:
JSON
{
  "url": "https://dashboard.hifibridge.com/accept-terms-of-service?sessionToken=536bb03a-8ac9-4ba7-b928-461007ecf6eb&templateId=2fb2da24-472a-4e5b-b160-038d9dc82a40&sandbox=true",
  "signedAgreementId": "8cb49537-bcf9-41b1-8d8c-c9c200d7341b"
}
You will get a response object back containing the url and the signedAgreementId. The url directs you to HIFI’s Terms of Service page. The signedAgreementId is the idempotencyKey you passed in, which will be valid only after you accept the Terms of Service.
HIFI will trigger the TOS_LINK.UPDATE webhook event as soon as the user clicks the accept button.

Create User

Now that we have the user’s valid signedAgreementId and basic personal information, we can create a user by calling the Create User endpoint. We have provided all the basic personal information with dummy values in the curl request. Request:
curl --request POST \
     --url https://sandbox.hifibridge.com/v2/users \
     --header 'accept: application/json' \
     --header 'authorization: Bearer YOUR-API-KEY' \
     --header 'content-type: application/json' \
     --data '
{
  "type": "individual",
  "address": {
    "addressLine1": "123 Main st",
    "city": "New York",
    "stateProvinceRegion": "NY",
    "postalCode": "10010",
    "country": "USA"
  },
  "requestId": "705f1f8b-a080-467c-b683-174eca409928",
  "firstName": "John",
  "lastName": "Doe",
  "email": "john@hifibridge.com",
  "dateOfBirth": "1999-01-01",
  "signedAgreementId": "2415fed7-ce1c-4752-a67e-8ed1bb4a1a0d"
}
'
Response:
{
  "id": "32051b2f-0798-55a7-9c42-b08da4192c97",
  "type": "individual",
  "email": "john@hifibridge.com",
  "name": "John Doe",
  "wallets": {
    "INDIVIDUAL": {
      "POLYGON": {
        "address": "0x1b932E54e77Aeb698144550d5a493Ea99E20Daa7"
      },
      "ETHEREUM": {
        "address": "0xC1c767eaB34b3Cc2C33a354f6Ff2c20fCB98D3C9"
      }
    }
  }
}
Let’s take a moment to understand the response:
  • The id is the user ID, which should be saved for future API calls for this particular user.
  • The wallets object contains all the wallet types and addresses provisioned for the user.
We have successfully created a user with the user id: 32051b2f-0798-55a7-9c42-b08da4192c97.

KYC

After successfully creating a user, the user needs to decide which rails to unlock and submit KYC to enable access to those rails. In this guide, we will focus on the USD rail. To read more about the rails we support, click here To unlock, the USD rail, follow these steps:
  1. Gather the required KYC information for the USD rail by either consulting our KYC documentation or using the Retrieve KYC Requirements endpoint.
  2. Update the user’s KYC information using the Update KYC Information endpoint.
  3. Upload documentation using the Add Documents endpoint.
  4. Submit the user’s KYC information through the Submit KYC endpoint to unlock the rail.
  5. Check the user’s KYC status for the USD rail via the Retrieve KYC Status endpoint.
Let’s go through each of these steps in detail to unlock the USD rail.

Retrieve KYC Requirements

The Retrieve KYC Requirements endpoint provides the required and optional KYC fields needed to unlock a specific rail, as well as any invalid or missing KYC information the user currently holds, assuming they intend to submit KYC for this rail. This information helps identify what needs to be updated via the Update KYC endpoint before submitting KYC for the rail. Let’s get the KYC requirements for USD rails by calling the Retrieve KYC Requirements endpoint and passing in USD as the rails and the userId. Request:
curl --request GET \
     --url 'https://sandbox.hifibridge.com/v2/users/32051b2f-0798-55a7-9c42-b08da4192c97/kyc/requirements?rails=USD' \
     --header 'accept: application/json' \
     --header 'authorization: Bearer YOUR-API-KEY'
Response:
{
  "userId": "65fe5742-c7cf-5bd6-aaa3-f372d3fe45a1",
  "rails": "USD",
  "type": "individual",
  "required": {
    "firstName": "string",
    "lastName": "string",
    "email": "string",
    "phone": "string",
    "nationality": "string",
    "dateOfBirth": "date",
    "taxIdentificationNumber": "string",
    "documents": {
      "identity": {
        "minCount": 1,
        "acceptedDocTypes": [
          "DRIVERS",
          "ID_CARD",
          "PASSPORT",
          "RESIDENCE_PERMIT"
        ]
      }
    }
  },
  "optional": {},
  "invalid": {
    "message": "fields are either missing or invalid",
    "fields": {
      "phone": "missing",
      "nationality": "missing",
      "taxIdentificationNumber": "missing"
    },
    "documents": {
      "message": "The following document groups are not satisfied: identity",
      "groups": {
        "identity": {
          "minCount": 1,
          "acceptedDocTypes": [
            "DRIVERS",
            "ID_CARD",
            "PASSPORT",
            "RESIDENCE_PERMIT"
          ]
        }
      }
    }
  }
}
Let’s take a moment to understand the response:
  • The required fields represents all the mandatory KYC information needed for the USD rails KYC application.
  • The optional fields represents all the additional KYC information that is not mandatory but may be provided for the USD_EURO rails KYC application.
  • The invalid represent any fields that must be corrected before the KYC application can proceed for the USD rail. From the response, we can see that the user still have multiple missing KYC fields that needs to be provided before they can submit their KYC application for the rail.
Now that we know the missing KYC fields the user needs to provide for the rail, we can proceed with updating the user’s KYC information to address these gaps.

Update KYC

Update KYC Information

To update the user’s KYC information, you can use the Update KYC endpoint. Let’s update the user’s KYC information. We have provided all the needed KYC fields with dummy values in the curl request. Request:
curl --request POST \
     --url https://sandbox.hifibridge.com/v2/users/32051b2f-0798-55a7-9c42-b08da4192c97/kyc \
     --header 'accept: application/json' \
     --header 'authorization: Bearer YOUR-API-KEY' \
     --header 'content-type: application/json' \
     --data '
{
    "phone": "+8573491112",
    "taxIdentificationNumber": "725569852",
    "nationality": "USA",
}
'
Response:
{
  "userId": "32051b2f-0798-55a7-9c42-b08da4192c97",
  "kycInfo": {
    "type": "individual",
    "firstName": "John",
    "lastName": "Doe",
    "nationality": "USA",
    "email": "john@hifibridge.com",
    "phone": "+8573491112",
    "address": {
      "city": "New York",
      "country": "USA",
      "postalCode": "10010",
      "addressLine1": "123 Main st",
      "stateProvinceRegion": "NY"
    },
    "dateOfBirth": "1999-01-01T00:00:00+00:00",
    "taxIdentificationNumber": "725569852",
    "documents": []
  }
}
The response contains the latest KYC information the user holds after the update. This same set of information can also be retrieved using the Retrieve KYC information endpoint.

Upload KYC Documentation

We have successfully updated the user’s KYC information now, let’s also upload the documentation. Users can upload raw documentation directly via the Upload a File endpoint. HIFI will return a file ID, which can later be used to attach the document to the user. Upload Documentation Request
curl --request POST \
     --url https://sandbox.hifibridge.com/v2/files \
     --header 'accept: application/json' \
     --header 'authorization: Bearer YOUR-API-KEY' \
     --header 'content-type: multipart/form-data' \
     --form file='@Screenshot%202025-09-23%20at%2011.39.37%E2%80%AFPM.png'
Upload Documentation Response
{
  "id": "file_JzALYV2L1-4LBmaxZ6GCm",
  "createdAt": "2025-09-25T22:05:16.764Z",
  "fileName": "Screenshot%202025-09-23%20at%2011.39.37%E2%80%AFPM.png",
  "size": 17834,
  "mimeType": "image/png"
}
After receiving the fileId, you can attach the documentation to the user. For the documentation required for the USD rail, please refer to Individual Documents . In this example, we will use the previously uploaded file as the user’s driver’s license to satisfy the identity requirement. Add Documentation Request
curl --request POST \
     --url https://sandbox.hifibridge.com/v2/users/32051b2f-0798-55a7-9c42-b08da4192c97/kyc/documents \
     --header 'accept: application/json' \
     --header 'authorization: Bearer YOUR-API-KEY' \
     --header 'content-type: application/json' \
     --data '
[
  {
    "type": "DRIVERS",
    "subType": "FRONT_SIDE",
    "issuedCountry": "USA",
    "fileId": "file_JzALYV2L1-4LBmaxZ6GCm"
  },
  {
    "type": "DRIVERS",
    "subType": "BACK_SIDE",
    "issuedCountry": "USA",
    "fileId": "file_JzALYV2L1-4LBmaxZ6GCm"
  }
]
'
Add Documentation Response
{
  "count": 2,
  "data": [
    {
      "id": "e477fa56-3e1e-4dac-abe6-008166749f30",
      "type": "DRIVERS",
      "subType": "FRONT_SIDE",
      "issuedCountry": "USA",
      "url": "https://pqgnrjvoqbopfaxmlhlv.supabase.co/storage/v1/object/sign/kyc_documents/32051b2f-0798-55a7-9c42-b08da4192c97/DRIVERS-FRONT_SIDE/cca55ddd-b0f9-453a-bcb4-fbad7af7a974?token=eyJraWQiOiJzdG9yYWdlLXVybC1zaWduaW5nLWtleV9mOTU1NGU4Ni01MDE4LTRiOGEtODY1Zi03NjhmYTc2M2MxZWQiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJreWNfZG9jdW1lbnRzLzMyMDUxYjJmLTA3OTgtNTVhNy05YzQyLWIwOGRhNDE5MmM5Ny9EUklWRVJTLUZST05UX1NJREUvY2NhNTVkZGQtYjBmOS00NTNhLWJjYjQtZmJhZDdhZjdhOTc0IiwiaWF0IjoxNzU4ODM4Mjc1LCJleHAiOjE3NTg4NDE4NzV9.0EuLRGSBcpp9zAOphpRu8D44J3zEFDZ57n61WY0n0kw",
      "fileId": "file_JzALYV2L1-4LBmaxZ6GCm",
    },
    {
      "id": "a2190d1d-61af-416d-8fcf-20673da5eaa0",
      "type": "DRIVERS",
      "subType": "BACK_SIDE",
      "issuedCountry": "USA",
      "url": "https://pqgnrjvoqbopfaxmlhlv.supabase.co/storage/v1/object/sign/kyc_documents/32051b2f-0798-55a7-9c42-b08da4192c97/DRIVERS-BACK_SIDE/77e5e45a-2cfe-4b6b-b9e1-dc14aa43897e?token=eyJraWQiOiJzdG9yYWdlLXVybC1zaWduaW5nLWtleV9mOTU1NGU4Ni01MDE4LTRiOGEtODY1Zi03NjhmYTc2M2MxZWQiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJreWNfZG9jdW1lbnRzLzMyMDUxYjJmLTA3OTgtNTVhNy05YzQyLWIwOGRhNDE5MmM5Ny9EUklWRVJTLUJBQ0tfU0lERS83N2U1ZTQ1YS89Y2ZlLTRiNmItYjllMS1kYzE0YWE0Mzg5N2UiLCJpYXQiOjE3NTg4MzgyNzUsImV4cCI6MTc1ODg0MTg3NX0.tWNS0tmtXbZGk5PvtLhrzOOTnvi56QsbGvl9ZWvGN7Q",
      "fileId": "file_JzALYV2L1-4LBmaxZ6GCm",
    }
  ]
}
Great! Now that we have updated all the required KYC information for the user, we can move on to submitting the KYC application.

Submit KYC

To submit the KYC information the user currently holds for the USD rails, we can use the Submit KYC endpoint.
📘 The Submit KYC endpoint submits the existing KYC information stored for the user to the specified rail. If you want to submit any new KYC data that the user doesn’t currently hold, you must first update the user’s KYC information using the Update KYC endpoint before calling Submit KYC.
Let’s submit the user’s KYC information to unlock the USD_EURO rails. This can be done by calling the Submit KYC endpoint and providing the userId and the rails. Request:
curl --request POST \
     --url https://sandbox.hifibridge.com/v2/users/32051b2f-0798-55a7-9c42-b08da4192c97/kyc/submissions \
     --header 'accept: application/json' \
     --header 'authorization: Bearer YOUR-API-KEY' \
     --header 'content-type: application/json' \
     --data '{"rails":"USD"}'
Response:
{
  "USD": {
    "status": "CREATED",
    "message": "Your KYC application has been successfully created. We will review it shortly."
  }
}
After submission, the KYC status will initially be set to "CREATED". The user can either call the Retrieve KYC Status endpoint or wait for webhook events to receive updates on the user’s latest KYC status for that rail.

Retrieve KYC Status

To get the KYC status for a specific rails, the user can call the Retrieve KYC Status endpoint. Let’s Get the KYC status for the user’s USD rail by passing in the userId and the rails in the query param. Request:
curl --request GET \
     --url 'https://sandbox.hifibridge.com/v2/users/32051b2f-0798-55a7-9c42-b08da4192c97/kyc/status?rails=USD' \
     --header 'accept: application/json' \
     --header 'authorization: Bearer YOUR-API-KEY' \
     --header 'content-type: application/json'
Response:
{
  "status": "ACTIVE",
  "message": "",
  "reviewResult": {
    "reviewAnswer": "APPROVED",
    "reviewRejectType": "",
    "rejectReasons": [],
    "comment": ""
  },
  "details": {
    "identity": {
      "reviewResult": {
        "reviewAnswer": "APPROVED",
        "reviewRejectType": "",
        "rejectReasons": [],
        "comment": ""
      },
      "details": [
        {
          "id": "e477fa56-3e1e-4dac-abe6-008166749f30",
          "type": "DRIVERS",
          "subType": "FRONT_SIDE",
          "reviewResult": {
            "reviewAnswer": "APPROVED",
            "reviewRejectType": "",
            "rejectReasons": [],
            "comment": ""
          }
        },
        {
          "id": "a2190d1d-61af-416d-8fcf-20673da5eaa0",
          "type": "DRIVERS",
          "subType": "BACK_SIDE",
          "reviewResult": {
            "reviewAnswer": "APPROVED",
            "reviewRejectType": "",
            "rejectReasons": [],
            "comment": ""
          }
        }
      ]
    },
    "questionnaire": {
      "reviewResult": {
        "reviewAnswer": "APPROVED",
        "reviewRejectType": "",
        "rejectReasons": [],
        "comment": ""
      }
    },
    "personalInfo": {
      "reviewResult": {
        "reviewAnswer": "APPROVED",
        "reviewRejectType": "",
        "rejectReasons": [],
        "comment": ""
      }
    }
  }
}
The returned object will contain the user’s latest KYC status for the USD rail, which will be "ACTIVE" if KYC is approved. In the sandbox environment, KYC approval should occur automatically.

Account

After creating a user who has passed KYC, you can add virtual accounts or offramp accounts for them to enable onramp or offramp transfers. The USD rail supports services for both onramp and offramp.
  • Virtual account: A bank account used to collect fiat currency deposits from your end users. Once the funds are received, they will be automatically converted to cryptocurrency.
  • Offramp account: A bank account used as the destination for the offramping process. For example, during offramping, stablecoin is converted into fiat currency and sent to the offramp account.
We will now add both virtual account and offramp accounts for the user.

Add Virtual Account

A Virtual Account is a bank account created by our system to facilitate onramp. Users can deposit fiat money into the virtual account, and the deposited funds are automatically converted into stablecoin. The parameters you pass in will determine the rail you want this onramp virtual account to support. For example, passing the sourceCurrency as usd, destinationCurrency as usdc, and destinationChain as POLYGON, will allow the user to deposit usd into the virtual bank account to onramp to usdc on POLYGON. Let’s make a Create a virtual account call using the user id we created earlier, with the parameters we just mentioned: Request
curl --request POST \
     --url https://sandbox.hifibridge.com/v2/users/32051b2f-0798-55a7-9c42-b08da4192c97/virtual-accounts \
     --header 'accept: application/json' \
     --header 'authorization: Bearer YOUR-API-KEY' \
     --header 'content-type: application/json' \
     --data '
{
  "sourceCurrency": "usd",
  "destinationCurrency": "usdc",
  "destinationChain": "POLYGON"
}
'
Response
{
  "message": "Virtual account created successfully",
  "accountInfo": {
    "id": "938e3b36-3be7-5535-ba12-8d89eb683e6b",
    "userId": "32051b2f-0798-55a7-9c42-b08da4192c97",
    "source": {
      "paymentRail": [
        "ach",
        "wire",
        "rtp"
      ],
      "currency": "usd"
    },
    "destination": {
      "chain": "POLYGON",
      "currency": "usdc",
      "walletAddress": "0x1b932E54e77Aeb698144550d5a493Ea99E20Daa7",
      "externalWalletId": null
    },
    "status": "activated",
    "microDeposits": {
      "count": 0,
      "data": []
    },
    "depositInstructions": {
      "bankName": "Cross River Bank",
      "bankAddress": "885 Teaneck Road, Teaneck, NJ 07666",
      "beneficiary": {
        "name": "John Doe",
        "address": "123 Main st, New York, NY, 10010, US"
      },
      "ach": {
        "routingNumber": "021214891",
        "accountNumber": "344176915009"
      },
      "wire": {
        "routingNumber": "021214891",
        "accountNumber": "344176915009"
      },
      "rtp": {
        "routingNumber": "021214891",
        "accountNumber": "344176915009"
      },
      "reference": null,
      "depositBy": null,
      "instruction": "Please deposit usd to the bank account provided. Please ensure that the beneficiary name matches the account holder name provided, or the payment may be rejected."
    },
    "settlementRuleId": null
  }
}
Let’s take a look at the response, focusing on the accountInfo object:
  • The id is the unique identifier for the newly created virtual account. This ID should be saved for future retrieval of account information, including deposit instructions and micro-deposit details required by the institution.
  • The source.paymentRail indicates the payment methods supported by this virtual account.
  • The source.currency, destination.chain, and destination.currency together represents the complete onramp rail. In our case, any usd deposited into the virtual account will be converted to usdc and sent to destination.walletAddress on the POLYGON blockchain.
  • The status reflects whether this virtual account is active for onramping.
  • IMPORTANT: The depositInstructions object contains the bank account details that the user needs to deposit fiat into for onramping.

Add Offramp Account

To offramp, you can add a USD offramp bank account by making an Create a USD Offramp Bank Account call. Let’s add a USD offramp bank account for Wire. To do this, you’ll need to provide your bank account details. However, for the purpose of this guide, we’ve pre-configured the bank account details for you, so all you need to do is call the Create a USD Offramp Bank Account endpoint: Request:
curl --request POST \
     --url https://sandbox.hifibridge.com/v2/users/32051b2f-0798-55a7-9c42-b08da4192c97/accounts \
     --header 'accept: application/json' \
     --header 'authorization: Bearer YOUR-API-KEY' \
     --header 'content-type: application/json' \
     --data '
{
    "rail": "offramp",
    "type": "us",
    "accountHolder": {
        "type": "individual",
        "name": "Henry Wu",
        "phone": "+2347072688123",
        "email": "henry@hifibridge.com",
        "address": {
            "addressLine1": "Example St 1.",
            "city": "Hoboken",
            "stateProvinceRegion": "NJ",
            "postalCode": "07030",
            "country": "USA"
        }
    },
    "us": {
        "transferType": "wire",
        "accountType": "Checking",
        "accountNumber": "99485843",
        "routingNumber": "011002877",
        "bankName": "HIFI Bank",
        "currency": "usd"
    }
}
'
Response:
{
    "status": "ACTIVE",
    "invalidFields": [],
    "message": "Account created successfully",
    "id": "583eb259-e78b-4f0c-a4b5-a8957876fa6f"
}
The id returned in the response object is the unique identifier for the USD offramp bank account. This ID should be saved for future use whenever you want to initiate an offramp through a Wire transfer.

Transfer

After creating both virtual accounts and offramp accounts, the user can now perform three types of transfers/conversions:
  • Onramp Fiat to Stablecoin: Convert fiat currency from an onramp bank account to stablecoin.
  • Stablecoin Transfer: Transfer stablecoin between users or wallet addresses.
  • Offramp Stablecoin to Fiat: Convert stablecoin to fiat currency and send it to an offramp bank account.
In this section of the quick start guide, we will walk through the entire transfer flow from onramp to offramp between two users. The first user (User A) will be the user we just created, and the second user (User B) will be an existing user we’ve provided for the purpose of this guide. Here’s how the entire transfer flow will look like in three steps:
  1. Onramp $2 USD and convert to 2 USDC into User A’s wallet.
  2. Transfer the 1 USDC from User A’s wallet to User B’s wallet.
  3. Offramp User A’s 1 USDC to User A’s bank account as $1 USD.
📘 Please note that in the sandbox environment, no real money movement occurs, so the onramping and offramping won’t actually process real funds. However, all the request and response examples will provide a clear overview of how the transfer occurs.

Onramp $2 USD to 2 USDC

Onramping fiat to stablecoin via a virtual account requires your end user to send US Dollars to the virtual account we just created. For example, if the user wants to send USD via Wire, they can use their bank app or contact their bank to send $1 USD to the following destination:
  • Bank Name: Cross River Bank
  • Beneficiary Name: John Doe
  • Beneficiary Address: 123 Main St, New York, NY, 10010, US
  • Routing Number: 021214891
  • Account Number: 344176915009
In production, the entire flow is only triggered after the user makes a deposit and we confirm that the fiat has landed. In the sandbox, we provide a simulation endpoint to mimic the behavior of an incoming deposit. Below is how we can create a simulated virtual account deposit Request:
curl --request POST \
     --url https://sandbox.hifibridge.com/v2/users/32051b2f-0798-55a7-9c42-b08da4192c97/virtual-accounts/938e3b36-3be7-5535-ba12-8d89eb683e6b/simulate-deposit \
     --header 'accept: application/json' \
     --header 'authorization: Bearer YOUR-API-KEY' \
     --header 'content-type: application/json' \
     --data '
{
  "paymentRail": "WIRE",
  "source": {
    "routingNumber": "021000021",
    "accountNumber": "516843515316",
    "name": "Jack Doe",
    "bankName": "JP Morgan Chase"
  },
  "amount": "2",
  "requestId": "32639f89-5fcc-4e31-8abe-0e710ba2e4a1",
  "reference": "This is a test deposit"
}
'
Response:
{
  "message": "Sandbox deposit triggered"
}
Now that we have successfully created a deposit simulation in the sandbox, the next step is to listen for our webhook messages for ONRAMP.CREATE. In the webhook events, you should see a new onramp transaction created for the deposit we just simulated.
    {
      "transferType": "ONRAMP",
      "transferDetails": {
        "id": "47f14814-cf3f-4e04-bccf-e3f8e6f5c4b9",
        "requestId": "8e02ad32-2fdb-4757-876f-9f7183ca595b",
        "createdAt": "2025-09-26T02:52:01.129Z",
        "updatedAt": "2025-09-26T02:52:01.129Z",
        "status": "FIAT_PENDING",
        "failedReason": null,
        "source": {
          "userId": "32051b2f-0798-55a7-9c42-b08da4192c97",
          "currency": "usd",
          "amount": 2,
          "accountId": null,
          "user": {
            "email": "john@hifibridge.com",
            "lastName": "Doe",
            "firstName": "John",
            "businessName": null
          },
          "bankInfo": {
            "bankName": "JP MORGAN CHASE",
            "senderName": "Jack Doe",
            "routingNumber": "021000021",
            "accountNumber": "516843515316",
            "imad": "20250925SIM3E6A1638944",
            "omad": "601040282819436691228450357132228324",
            "bankAddress": "257 Dalton Groves, Barton City, MI 48075",
            "description": "THIS IS A TEST D",
            "paymentRail": "wire"
          }
        },
        "destination": {
          "userId": "32051b2f-0798-55a7-9c42-b08da4192c97",
          "currency": "usdc",
          "chain": "POLYGON",
          "walletAddress": "0x1b932E54e77Aeb698144550d5a493Ea99E20Daa7",
          "externalWalletId": null,
          "amount": null,
          "user": {
            "email": "john@hifibridge.com",
            "lastName": "Doe",
            "firstName": "John",
            "businessName": null
          }
        },
        "receipt": {
          "transactionHash": null
        },
        "developerFee": null,
        "virtualAccountId": "938e3b36-3be7-5535-ba12-8d89eb683e6b",
        "quoteInformation": {
          "sendGross": {
            "amount": "2.00",
            "currency": "usd"
          },
          "sendNet": {
            "amount": "2.00",
            "currency": "usd"
          },
          "railFee": {
            "amount": "0.00",
            "currency": "usdc"
          },
          "receiveGross": {
            "amount": "2.00",
            "currency": "usdc"
          },
          "receiveNet": {
            "amount": "2.00",
            "currency": "usdc"
          },
          "rate": "1.00",
          "expiresAt": "N/A"
        },
        "depositInfo": null
      }
    }
After the onramp transaction status is updated to COMPLETE, you can check the balance of the user’s wallet to see the testnet USDC we just onramped!
The bankInfo under the source object contains the information of the sender and the sending bank.

Transfer 1 USDC Between Wallets

After the onramp transfer is completed, User A will have 1 USDC in their wallet. Now we can transfer the 1 USDC from User A’s wallet to User B’s wallet. To do this, we can call the Create a crypto transfer endpoint. Let’s take a look at the request fields:
  • The requestId is a unique identifier for the transfer request, ensuring that a request is processed only once to prevent duplicates.
  • The source.userId represents the user who wants to send the stablecoin. In our case, this is User A’s user id.
  • The destination.userId represents the user who will receive the stablecoin. In our case, this is User B’s user id.
  • The chain, currency, and amount fields indicate that we are sending 1 usdc on the POLYGON blockchain.
Request:
curl --request POST \
     --url https://sandbox.hifibridge.com/v2/wallets/transfers \
     --header 'accept: application/json' \
     --header 'authorization: Bearer YOUR-API-KEY' \
     --header 'content-type: application/json' \
     --data '
{
  "source": {
    "userId": "32051b2f-0798-55a7-9c42-b08da4192c97"
  },
  "destination": {
    "userId": "30669fcc-b15e-4137-b4fc-9e8f7f659a87"
  },
  "requestId": "a40ea2aa-7937-4be9-bb1f-b75f1489bcc6",
  "amount": 1,
  "currency": "usdc",
  "chain": "POLYGON"
}
'
Response:
{
  "transferType": "WALLET.TRANSFER",
  "transferDetails": {
    "id": "1a1ad1dd-ad72-4f3f-910b-c45dcf09875f",
    "requestId": "32051b2f-0798-55a7-9c42-b08da4192c97",
    "createdAt": "2025-09-26T03:04:11.092Z",
    "updatedAt": "2025-09-26T03:04:11.092Z",
    "chain": "POLYGON",
    "currency": "usdc",
    "contractAddress": "0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582",
    "status": "CREATED",
    "failedReason": null,
    "source": {
      "userId": "32051b2f-0798-55a7-9c42-b08da4192c97",
      "walletAddress": "0x1b932E54e77Aeb698144550d5a493Ea99E20Daa7",
      "walletType": "INDIVIDUAL",
      "user": {
        "email": "john@hifibridge.com",
        "lastName": "Doe",
        "firstName": "John",
        "businessName": null
      }
    },
    "destination": {
      "userId": "30669fcc-b15e-4137-b4fc-9e8f7f659a87",
      "walletAddress": "0x1b932E54e77Aeb688144550d5a493Ea99E20Daa7",
      "user": {
        "email": "jack@hifibridge.com",
        "lastName": "Doe",
        "firstName": "Jack",
        "businessName": null
      }
    },
    "amount": 1,
    "amountIncludeDeveloperFee": 1,
    "receipt": {
      "transactionHash": null,
      "userOpHash": null
    }
  }
}
Let’s take a look at the response object, focusing on the transferDetails object:
  • The id is a unique identifier for this transfer, which you can save to get the most up-to-date transfer status using the Retrieve a crypto transfer endpoint.
  • The status represents the transfer status, which initially appears as "CREATED". You will want to either register a webhook or poll the Retrieve a crypto transfer endpoint to monitor the latest transfer status until the transfer is "COMPLETED".
  • The receipt.transactionHash is the transaction hash on the blockchain, which you can use to check the transaction status online with a blockchain explorer.
  • The failedReason will show the reason if the transfer fails.
  • The sender and recipient objects provide detailed information about the sender and recipient users.

Offramp 1 USDC to $ 1 USD

Now let’s offramp the rest of the 1 USDC to userA’s bank account. To do this, we can call the Create an offramp endpoint. Let’s take a look at the request fields:
  • The requestId is a unique identifier for the transfer request, ensuring that a request is processed only once to prevent duplicates.
  • The source.userId represents the user from whom we want to offramp. In our case, this is User A’s user id.
  • The destination.userId represents the user receiving the offramp funds, and the destination.accountId is the receiving offramp account owned by destination.userId. In our case, the destination.userId will be User A’s user id, and the destination.accountId will be User A’s USD offramp account id.
  • The chain, source.currency, destination.currency, and source.amount fields indicate that we are converting 1 usdc on the POLYGON blockchain to usd and sending that usd to destination.accountId.
Request:
curl --request POST \
     --url https://sandbox.hifibridge.com/v2/offramps \
     --header 'accept: application/json' \
     --header 'authorization: Bearer YOUR-API-KEY' \
     --header 'content-type: application/json' \
     --data '
{
  "source": {
    "currency": "usdc",
    "chain": "POLYGON",
    "userId": "32051b2f-0798-55a7-9c42-b08da4192c97",
    "amount": 1
  },
  "destination": {
    "currency": "usd",
    "accountId": "583eb259-e78b-4f0c-a4b5-a8957876fa6f",
    "userId": "32051b2f-0798-55a7-9c42-b08da4192c97",
  },
  "requestId": "b08e27be-c086-4b84-a321-307ed9f265e1"
}
'
Response:
{
  "transferType": "OFFRAMP",
  "transferDetails": {
    "id": "b838908b-95d0-4ebb-a2c6-8f0c142bcdd7",
    "requestId": "32051b2f-0798-55a7-9c42-b08da4192c97",
    "createdAt": "2025-09-26T03:15:57.766Z",
    "updatedAt": "2025-09-26T03:15:58.463Z",
    "status": "OPEN_QUOTE",
    "failedReason": null,
    "error": null,
    "errorDetails": null,
    "source": {
      "userId": "32051b2f-0798-55a7-9c42-b08da4192c97",
      "chain": "POLYGON_AMOY",
      "currency": "usdc",
      "amount": 1,
      "walletAddress": "0x1b932E54e77Aeb698144550d5a493Ea99E20Daa7",
      "user": {
        "email": "john@hifibridge.com",
        "lastName": "Doe",
        "firstName": "John",
        "businessName": null
      }
    },
    "destination": {
      "userId": "32051b2f-0798-55a7-9c42-b08da4192c97",
      "amount": 1,
      "currency": "usd",
      "user": {
        "email": "john@hifibridge.com",
        "lastName": "Doe",
        "firstName": "John",
        "businessName": null
      },
      "accountId": "583eb259-e78b-4f0c-a4b5-a8957876fa6f"
    },
    "receipt": {
      "transactionHash": null
    },
    "developerFee": null,
    "quoteInformation": {
      "rate": "1",
      "sendNet": {
        "amount": "1.00",
        "currency": "usdc"
      },
      "sendGross": {
        "amount": "1.00",
        "currency": "usdc"
      },
      "receiveNet": {
        "amount": "1.00",
        "currency": "usd"
      },
      "receiveGross": {
        "amount": "1.00",
        "currency": "usd"
      },
      "expiresAt": "2025-09-27T03:15:58.112Z"
    },
    "depositInformation": []
  }
}
Let’s take a look at the response object, focusing on the transferDetails object:
  • The id is a unique identifier for this transfer, which you can save to get the most up-to-date transfer status using the Retrieve an offramp endpoint.
  • The status represents the transfer status, which initially appears as "OPEN_QUOTE". You will have to call the Accept Quote Endpoint to kick off the offramp process
  • The receipt.transactionHash is the transaction hash on the blockchain, which you can use to check the transaction status online with a blockchain explorer.
  • The error and errorDetails will show the reason if the transfer fails.
  • The sourceUser and destinationUser objects provide detail information about the sender and recipient users.
  • The quoteInformation object provides the conversion rate information between the source and destination currencies.
After confirming the quote, call the Accept Quote Endpoint to start the rest of the offramp process before the quote expires, as indicated by the expiresAt field. Request:
curl --request POST \
     --url https://sandbox.hifibridge.com/v2/offramps/b838908b-95d0-4ebb-a2c6-8f0c142bcdd7/quote/accept \
     --header 'accept: application/json' \
     --header 'authorization: Bearer YOUR-API-KEY'
Response
{
  "transferType": "OFFRAMP",
  "transferDetails": {
    "id": "b838908b-95d0-4ebb-a2c6-8f0c142bcdd7",
    "requestId": "32051b2f-0798-55a7-9c42-b08da4192c97",
    "createdAt": "2025-09-26T03:15:57.766Z",
    "updatedAt": "2025-09-26T03:15:58.463Z",
    "status": "CRYPTO_INITIATED",
    "failedReason": null,
    "error": null,
    "errorDetails": null,
    "source": {
      "userId": "32051b2f-0798-55a7-9c42-b08da4192c97",
      "chain": "POLYGON_AMOY",
      "currency": "usdc",
      "amount": 1,
      "walletAddress": "0x1b932E54e77Aeb698144550d5a493Ea99E20Daa7",
      "user": {
        "email": "john@hifibridge.com",
        "lastName": "Doe",
        "firstName": "John",
        "businessName": null
      }
    },
    "destination": {
      "userId": "32051b2f-0798-55a7-9c42-b08da4192c97",
      "amount": 1,
      "currency": "usd",
      "user": {
        "email": "john@hifibridge.com",
        "lastName": "Doe",
        "firstName": "John",
        "businessName": null
      },
      "accountId": "9620f4b2-a3fc-4a86-a296-b10f0ae83e2c"
    },
    "receipt": {
      "transactionHash": null
    },
    "developerFee": null,
    "quoteInformation": {
      "rate": "1",
      "sendNet": {
        "amount": "1.00",
        "currency": "usdc"
      },
      "expiresAt": "2025-09-27T03:15:58.112Z",
      "sendGross": {
        "amount": "1.00",
        "currency": "usdc"
      },
      "receiveNet": {
        "amount": "1.00",
        "currency": "usd"
      },
      "receiveGross": {
        "amount": "1.00",
        "currency": "usd"
      }
    },
    "depositInformation": []
  }
}
Great! You should now see the status transition to CRYPTO_INITIATED. Next, you can either poll our Get Offramp Transaction endpoint to get the latest status or subscribe to the webhook to wait for the status update event.
Congratulations 🎉! You’ve successfully navigated several key processes, including creating a user, submitting KYC to unlock rails, adding onramp/offramp accounts, and transferring funds between users. We hope this guide has provided you with a solid understanding of how to utilize HIFI’s API endpoints to manage digital currency transfers seamlessly. If you have any further questions or need additional support, please refer to our documentation or contact our support team. Happy coding!
I