Off-ramp
Convert crypto to fiat and withdraw to a bank account.
Off-ramp pays out to a bank account in fiat instead of to a crypto address. The flow: pick a
country → register a bank account (once, reviewed) → quote → withdraw → track. Settlement is
asynchronous and reports via the same payout.* webhooks.
Before you start: the key needs payouts:write (reads need payouts:read); off-ramp is a fiat
payout, so it shares the payout scopes. There's a minimum of ~1,000 USDT-equivalent per order
(the quote enforces it), and the rail is enabled per workspace — if these endpoints return 404,
it isn't enabled on your account yet; contact support.
1. Countries & your banks
const countries = await ap.offramp.countries(); // [{ id: 1, name: "United States" }, …]
const banks = await ap.offramp.banks(); // your registered banks + review statuscountries = ap.offramp.countries() # [{ "id": 1, "name": "United States" }, …]
banks = ap.offramp.banks() # your registered banks + review statuscountries, err := ap.OffRamp.Countries(ctx) // [{ id: 1, name: "United States" }, …]
if err != nil {
log.Fatal(err)
}
banks, err := ap.OffRamp.Banks(ctx) // your registered banks + review status
if err != nil {
log.Fatal(err)
}
fmt.Println(countries, banks)curl "https://api.absolutepay.io/v1/offramp/countries" \
-H "Authorization: Bearer $APP_TOKEN" $(sign GET /v1/offramp/countries "")
curl "https://api.absolutepay.io/v1/offramp/banks" \
-H "Authorization: Bearer $APP_TOKEN" $(sign GET /v1/offramp/banks "")Note the numeric country id — you pass it as countryId when registering a bank.
2. Register a bank account
Off-ramp needs an approved bank account as the destination. Register one once with a
bank-statement image (jpg/png, ≤4MB, dated within 3 months — PDF is not accepted here), then
wait for review. Files ride as { filename, contentType, dataBase64 } (base64, no data-URL prefix).
const bank = await ap.offramp.registerBank({
bankAccountName: "Acme Inc",
bankName: "Bank of America",
countryId: 1, // numeric id from countries()
iban: "US64SVBKUS6S3300958879",
swift: "SVBKUS6S",
file: { filename: "statement.jpg", contentType: "image/jpeg", dataBase64: "…" },
});
// { bankAccountId, status: "pending" | "materials" | "approved" | "rejected", … }
// If review asks for more ("materials"), upload certificate + passport docs (PDF allowed here):
if (bank.status === "materials") {
await ap.offramp.submitBankMaterials(bank.bankAccountId as string, {
certificate: [{ filename: "license.pdf", contentType: "application/pdf", dataBase64: "…" }],
passport: [{ filename: "id.jpg", contentType: "image/jpeg", dataBase64: "…" }],
});
}
// Remove one you no longer need:
await ap.offramp.deleteBank("bank_123");bank = ap.offramp.register_bank(
bank_account_name="Acme Inc",
bank_name="Bank of America",
country_id=1, # numeric id from countries()
iban="US64SVBKUS6S3300958879",
swift="SVBKUS6S",
file={"filename": "statement.jpg", "contentType": "image/jpeg", "dataBase64": "…"},
)
# { bankAccountId, status: "pending" | "materials" | "approved" | "rejected", … }
# If review asks for more ("materials"), upload certificate + passport docs (PDF allowed here):
if bank["status"] == "materials":
ap.offramp.submit_bank_materials(
bank["bankAccountId"],
certificate=[{"filename": "license.pdf", "contentType": "application/pdf", "dataBase64": "…"}],
passport=[{"filename": "id.jpg", "contentType": "image/jpeg", "dataBase64": "…"}],
)
# Remove one you no longer need:
ap.offramp.delete_bank("bank_123")bank, err := ap.OffRamp.RegisterBank(ctx, absolutepay.BankParams{
BankAccountName: "Acme Inc",
BankName: "Bank of America",
CountryID: 1, // numeric id from Countries()
IBAN: "US64SVBKUS6S3300958879",
Swift: "SVBKUS6S",
File: absolutepay.DocFile{Filename: "statement.jpg", ContentType: "image/jpeg", DataBase64: "…"},
})
if err != nil {
log.Fatal(err)
}
// { bankAccountId, status: "pending" | "materials" | "approved" | "rejected", … }
// If review asks for more ("materials"), upload certificate + passport docs (PDF allowed here):
if bank["status"] == "materials" {
_, err = ap.OffRamp.SubmitBankMaterials(ctx, bank["bankAccountId"].(string), absolutepay.BankMaterialsParams{
Certificate: []absolutepay.DocFile{{Filename: "license.pdf", ContentType: "application/pdf", DataBase64: "…"}},
Passport: []absolutepay.DocFile{{Filename: "id.jpg", ContentType: "image/jpeg", DataBase64: "…"}},
})
if err != nil {
log.Fatal(err)
}
}
// Remove one you no longer need:
if err := ap.OffRamp.DeleteBank(ctx, "bank_123"); err != nil {
log.Fatal(err)
}BODY='{"bankAccountName":"Acme Inc","bankName":"Bank of America","countryId":1,"iban":"US64SVBKUS6S3300958879","swift":"SVBKUS6S","file":{"filename":"statement.jpg","contentType":"image/jpeg","dataBase64":"…"}}'
curl https://api.absolutepay.io/v1/offramp/banks \
-H "Authorization: Bearer $APP_TOKEN" -H "Content-Type: application/json" \
$(sign POST /v1/offramp/banks "$BODY") -d "$BODY"
# supplementary docs (when status is "materials"):
BODY='{"certificate":[{"filename":"license.pdf","contentType":"application/pdf","dataBase64":"…"}],"passport":[{"filename":"id.jpg","contentType":"image/jpeg","dataBase64":"…"}]}'
curl https://api.absolutepay.io/v1/offramp/banks/bank_123/materials \
-H "Authorization: Bearer $APP_TOKEN" -H "Content-Type: application/json" \
$(sign POST /v1/offramp/banks/bank_123/materials "$BODY") -d "$BODY"
# remove a bank:
curl -X DELETE "https://api.absolutepay.io/v1/offramp/banks/bank_123" \
-H "Authorization: Bearer $APP_TOKEN" $(sign DELETE /v1/offramp/banks/bank_123 "")Approval before withdrawal
Review is manual and asynchronous. A bank moves pending → approved, or to materials (more
documents needed — send both a certificate (business license) and a passport (government
ID) group, 1–5 files each), or rejected (the memo field carries the reason). Poll banks()
until the account is approved — only then can it be a withdraw destination.
3. Quote & withdraw
Quote the crypto → fiat conversion, then execute the withdrawal by echoing the quote back with your approved bank:
const quote = await ap.offramp.quote({
cryptoCurrency: "USDT",
fiatCurrency: "USD",
cryptoAmount: "1000",
});
// { quoteToken, validPeriod, cryptoAmount, fiatAmount, fiatRate, … }
const order = await ap.offramp.withdraw({
quoteToken: quote.quoteToken as string,
bankAccountId: "bank_123",
cryptoCurrency: "USDT",
fiatCurrency: "USD",
cryptoAmount: "1000",
fiatAmount: quote.fiatAmount as string,
});quote = ap.offramp.quote(
crypto_currency="USDT",
fiat_currency="USD",
crypto_amount="1000",
)
# { quoteToken, validPeriod, cryptoAmount, fiatAmount, fiatRate, … }
order = ap.offramp.withdraw(
quote_token=quote["quoteToken"],
bank_account_id="bank_123",
crypto_currency="USDT",
fiat_currency="USD",
crypto_amount="1000",
fiat_amount=quote["fiatAmount"],
)quote, err := ap.OffRamp.Quote(ctx, absolutepay.OffRampQuoteParams{
CryptoCurrency: "USDT",
FiatCurrency: "USD",
CryptoAmount: "1000",
})
if err != nil {
log.Fatal(err)
}
// { quoteToken, validPeriod, cryptoAmount, fiatAmount, fiatRate, … }
order, err := ap.OffRamp.Withdraw(ctx, absolutepay.OffRampWithdrawParams{
QuoteToken: quote["quoteToken"].(string),
BankAccountID: "bank_123",
CryptoCurrency: "USDT",
FiatCurrency: "USD",
CryptoAmount: "1000",
FiatAmount: quote["fiatAmount"].(string),
})
if err != nil {
log.Fatal(err)
}
fmt.Println(order)BODY='{"cryptoCurrency":"USDT","fiatCurrency":"USD","cryptoAmount":"1000"}'
curl https://api.absolutepay.io/v1/offramp/quote \
-H "Authorization: Bearer $APP_TOKEN" -H "Content-Type: application/json" \
$(sign POST /v1/offramp/quote "$BODY") -d "$BODY"
BODY='{"quoteToken":"qt_…","bankAccountId":"bank_123","cryptoCurrency":"USDT","fiatCurrency":"USD","cryptoAmount":"1000","fiatAmount":"921.00"}'
curl https://api.absolutepay.io/v1/offramp/withdraw \
-H "Authorization: Bearer $APP_TOKEN" -H "Content-Type: application/json" \
$(sign POST /v1/offramp/withdraw "$BODY") -d "$BODY"The quoteToken is short-lived (validPeriod is its validity in seconds) — if it expires, get a
fresh quote. All four currency/amount fields on withdraw must match the quote exactly.
4. Track
Orders move PENDING → DISPATCHED → SUCCESS | FAILED; settled orders carry a bankSlipInfo
receipt. Watch the payout.* webhooks and poll the (keyset-paginated) order list as
backup:
const page = await ap.offramp.orders({ limit: 50 });
// page.items, page.nextCursor — pass nextCursor back as `before` for the next pagepage = ap.offramp.orders(limit=50)
# page["items"], page["nextCursor"] — pass nextCursor back as `before` for the next pagepage, err := ap.OffRamp.Orders(ctx, absolutepay.PageQuery{Limit: 50})
if err != nil {
log.Fatal(err)
}
fmt.Println(page.Items, page.NextCursor) // pass NextCursor back as Before for the next pagecurl "https://api.absolutepay.io/v1/offramp/orders?limit=50" \
-H "Authorization: Bearer $APP_TOKEN" $(sign GET "/v1/offramp/orders?limit=50" "")Gotchas
- Minimum ~1,000 USDT-equivalent per order (maximum 3M) — the quote rejects anything below it.
- Withdrawals are irreversible once dispatched to the bank — double-check the
bankAccountId. - Document formats differ: registration takes a single statement image (jpg/png); only the
supplementary
materialsupload accepts PDF. Everything is base64, ≤4MB per file. - Amounts are decimal strings; the withdraw body must echo the quote's exact amounts.
- Pagination —
orders()is keyset-paginated (nextCursor→before) and accepts astatusfilter. - Sandbox — on
https://sandbox-api.absolutepay.iono real funds move; orders settle end-to-end automatically so you can test the full loop.
Full request/response fields are on the /v1/offramp/* endpoints in the API reference.
Next
- Send payouts — pay out to a crypto address instead.
- Conversions — consolidate assets before you off-ramp.
- Webhooks — receive settlement events.
- API reference · Authentication