AbsolutePayDocs

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 status
countries = ap.offramp.countries()  # [{ "id": 1, "name": "United States" }, …]
banks = ap.offramp.banks()  # your registered banks + review status
countries, 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 page
page = ap.offramp.orders(limit=50)
# page["items"], page["nextCursor"] — pass nextCursor back as `before` for the next page
page, 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 page
curl "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 materials upload accepts PDF. Everything is base64, ≤4MB per file.
  • Amounts are decimal strings; the withdraw body must echo the quote's exact amounts.
  • Paginationorders() is keyset-paginated (nextCursorbefore) and accepts a status filter.
  • Sandbox — on https://sandbox-api.absolutepay.io no 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

On this page