Skip to main content
Offramp lets your customers convert cryptocurrency into fiat currency deposited directly to their bank account. This guide covers the full implementation from crypto deposit to fiat settlement.

What is Offramp?

Offramp lets your customers convert cryptocurrency (USDT, USDC,etc) into fiat currency deposited to their bank account. The flow works like this:
  1. Customer sends crypto to a deposit address
  2. Coinut detects the deposit
  3. Coinut executes the trade
  4. Fiat arrives in the customer’s bank account

Architecture

Partner App -> Coinut API -> Deposit Address -> Deposit Detection -> Trade -> Fiat Bank Transfer

Prerequisites

Before starting an offramp flow, ensure:
  • Customer has APPROVED KYC status
  • Customer’s bank account has been added and verified
  • You have the bank account ID from the POST /bank-account/add response

Step-by-Step Implementation

1

Create Customer

Same as onramp. Call POST /customer/create with name, email, type="DEPOSITOR", isIndividual=true. Wait for CUSTOMER_APPROVED webhook.
2

Add Bank Account

Call POST /bank-account/add with the customer’s banking details.
{
  "customerId": "cust_abc123",
  "currency": "EUR",
  "bankName": "Deutsche Bank",
  "accountNo": "1234567890",
  "iban": "DE89370400440532013000",
  "swift": "DEUTDEFF",
  "accountName": "John Doe"
}
Store the returned bankAccountId. Required fields vary by currency — see the Bank Account Requirements table below.
3

Create Offramp Flow

Call POST /autoramp-flow/offramp with the crypto source and fiat destination:
{
  "from": {
    "customerId": "cust_abc123",
    "currency": "USDT",
    "network": "ETH"
  },
  "to": "bank_account_id_from_step_2"
}
This creates a deposit address and links incoming crypto to automatic fiat conversion.
4

Display Deposit Address

Retrieve the deposit address from the flow creation response or call GET /deposit-address/detail:
{
  "address": "0x742d35Cc6634C0532925a3b8D4C9db96590f6C7E",
  "network": "ETH",
  "currency": "USDT"
}
Display this address to the customer with a clear warning to send only on the specified network.
5

Customer Sends Crypto

Customer sends USDT to the displayed address on the specified network (ETH or TRX). Provide clear instructions:
  • Minimum deposit amount
  • Correct network only (wrong network = lost funds)
  • Include memo/tag if required
6

Monitor Deposits

Listen for webhook events:
  • DEPOSIT_RECEIVED — crypto detected on-chain
  • DEPOSIT_APPROVED — confirmations reached, deposit cleared
7

Track Trade

After deposit approval:
  • TRADE_CREATED — fiat trade initiated
  • TRADE_SETTLED — trade executed, fiat queued for bank transfer
8

Fiat Arrives

After TRADE_SETTLED, fiat is sent to the bank account via banking rails. Settlement time depends on currency:

Bank Account Requirements

CurrencyRequired FieldsOptional FieldsTypical Settlement
USDbankName, accountNo, aba OR swiftaccountName, bankAddress1-2 business days
EURbankName, accountNo OR iban, swiftaccountName, bankAddress1 business day
GBPbankName, accountNo OR iban, swiftaccountName, sortCode1 business day
CADbankName, accountNo, institutionNumber, transitaccountName1 business day
AUDbankName, accountNo, bsbaccountName1 business day
SGDbankName, accountNo, aba OR swiftaccountName, bankAddress1 business day

Complete Code Example

import os
import json
import hmac
import hashlib
import time
import uuid
import requests

BASE_URL = "https://ramp.hexarails.ai"
API_KEY = os.environ["COINUT_API_KEY"]
API_SECRET = os.environ["COINUT_API_SECRET"]


def create_signature(method: str, path: str, body: dict = None):
    timestamp = str(int(time.time()))
    nonce = str(uuid.uuid4())
    host = "ramp.hexarails.ai"
    request_body = "" if method.upper() == "GET" else json.dumps(body or {}, separators=(",", ":"))
    body_hash = hashlib.sha256(request_body.encode()).hexdigest() if request_body else ""
    payload = "\n".join([method.upper(), host, path, "", body_hash, timestamp, nonce])
    signature = hmac.new(API_SECRET.encode(), payload.encode(), hashlib.sha256).hexdigest()
    return timestamp, nonce, signature


def coinut_request(method: str, path: str, body: dict = None):
    timestamp, nonce, signature = create_signature(method, path, body)

    headers = {
        "Content-Type": "application/json",
        "X-API-Key": API_KEY,
        "X-Timestamp": timestamp,
        "X-Nonce": nonce,
        "X-Signature": signature,
    }

    resp = requests.request(method, f"{BASE_URL}{path}", json=body, headers=headers)
    resp.raise_for_status()
    return resp.json()


def create_customer(name: str, email: str, external_id: str) -> str:
    data = coinut_request("POST", "/customer/create", {
        "name": name,
        "email": email,
        "type": "DEPOSITOR",
        "isIndividual": True,
        "externalId": external_id,
    })
    return data["customerId"]


def add_bank_account(
    customer_id: str,
    currency: str,
    bank_name: str,
    account_no: str,
    **kwargs
) -> str:
    """Add a bank account. Required fields vary by currency."""
    body = {
        "customerId": customer_id,
        "currency": currency,
        "bankName": bank_name,
        "accountNo": account_no,
        **kwargs,
    }
    data = coinut_request("POST", "/bank-account/add", body)
    return data["bankAccountId"]


def create_offramp_flow(customer_id: str, network: str, bank_account_id: str):
    return coinut_request("POST", "/autoramp-flow/offramp", {
        "from": {
            "customerId": customer_id,
            "currency": "USDT", # "USDT" or "USDC"
            "network": network,  # "ETH","MATIC" or "TRX"
        },
        "to": bank_account_id,
    })


def execute_offramp(user: dict, bank_details: dict):
    """Full offramp flow for a user."""
    # Step 1: Create customer (wait for CUSTOMER_APPROVED webhook)
    customer_id = create_customer(user["name"], user["email"], user["id"])

    # Step 2: Add bank account
    bank_account_id = add_bank_account(
        customer_id=customer_id,
        currency=bank_details["currency"],
        bank_name=bank_details["bank_name"],
        account_no=bank_details["account_no"],
        swift=bank_details.get("swift"),
        iban=bank_details.get("iban"),
        bsb=bank_details.get("bsb"),
        aba=bank_details.get("aba"),
        accountName=user["name"],
    )

    # Step 3: Create offramp flow
    flow = create_offramp_flow(customer_id, bank_details["network"], bank_account_id)

    return {
        "customerId": customer_id,
        "bankAccountId": bank_account_id,
        "flowId": flow["flowId"],
        "depositAddress": flow["data"]["depositAddress"],
    }


# ── Webhook Handler (Flask) ──────────────────────────
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route("/webhooks/coinut", methods=["POST"])
def handle_webhook():
    event = request.json.get("event")
    data = request.json.get("data")

    handlers = {
        "DEPOSIT_RECEIVED": handle_deposit_received,
        "DEPOSIT_APPROVED": handle_deposit_approved,
        "TRADE_CREATED": handle_trade_created,
        "TRADE_SETTLED": handle_trade_settled,
    }

    handler = handlers.get(event)
    if handler:
        handler(data)

    return jsonify({"status": "ok"})


def handle_deposit_received(data):
    print(f"Deposit received: {data['amount']} {data['currency']}")
    # Notify customer that funds are being processed


def handle_deposit_approved(data):
    print(f"Deposit approved. Trade will execute shortly.")


def handle_trade_created(data):
    print(f"Trade created: {data['tradeId']}")


def handle_trade_settled(data):
    print(f"Trade settled! Fiat sent to bank account.")
    # Send customer notification


if __name__ == "__main__":
    app.run(port=3000)

Go-Live Checklist

  • Bank account validation working for all target currencies
  • Deposit address generation tested in sandbox
  • End-to-end flow verified with test transfers
  • Webhook handlers for all deposit and trade events
  • Error handling for insufficient funds and invalid addresses
  • Banking cutoff times documented for customer communication
  • Retry logic for failed bank transfers
Banking cutoff times vary by currency and institution. EUR SEPA transfers typically process until 3:00 PM CET. USD ACH transfers have a 5:00 PM ET cutoff. Communicate these timelines to your customers.