Custodial Off-Ramp
Custodial merchants only, sell BTC from the Uphold account and send USD to a registered bank account, including the Travel Rule compliance step when required. Amounts are denominated in satoshi (BTC debit) and USD (bank destination).
Preview off-ramp quote
https://api.gobtcpay.com/public/api/v1.2/merchant/custodial/offramp/quotePrices an off-ramp without starting one: what amountSats BTC becomes in USD on the bank account identified by bankAccountId, what the conversion costs, and how long that price holds. amountSats must be a whole number of satoshi greater than zero. Returns an approvedQuote, the signed terms offramp/create executes on (terms with merchantId, quoteId, amountSats, feeSats, netSats, bankAccountId, destinationAmount, destinationAsset, exchangeRate, expiresAt and travelRuleWillBeRequired, plus a signature over them), and a feeBreakdown for display. Satoshi values inside terms are decimal strings, which is what they are on the wire in both directions. Render every figure from terms: it is the only copy, so there is no second one to disagree with it. Store the approvedQuote exactly as received and send it back unmodified; read the countdown from approvedQuote.terms.expiresAt. Fails with quote_failed when the provider would not price it, never with a zero fee or a zero rate standing in for a price. Creates no off-ramp, reserves no balance and needs no 2FA, nothing moves until the merchant approves the quote by passing the whole approvedQuote to offramp/create.
Authorization: merchantAuth
Body required
application/jsonbankAccountIdstringrequired3fa85f64-5717-4562-b3fc-2c963f66afa6amountSatsstring | integerrequiredAmount in satoshis.
Responses
idstringrequired3fa85f64-5717-4562-b3fc-2c963f66afa6resultobjectrequiredcurl -X POST "$BASE_URL/merchant/custodial/offramp/quote" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{
"bankAccountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"amountSats": "string"
}'{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"result": {
"$case": "success",
"success": {
"approvedQuote": {
"terms": {
"merchantId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"quoteId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"amountSats": "string",
"feeSats": "string",
"netSats": "string",
"bankAccountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"destinationAmount": "string",
"destinationAsset": "string",
"exchangeRate": "string",
"expiresAt": 1718012345,
"travelRuleWillBeRequired": true
},
"signature": "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b"
},
"feeBreakdown": [
{
"type": "withdraw",
"code": "string",
"asset": "string",
"amount": "string",
"percentage": 0
}
]
}
}
}Create off-ramp
https://api.gobtcpay.com/public/api/v1.2/merchant/custodial/offramp/createExecutes an off-ramp the merchant has already approved: send back the approvedQuote returned by offramp/quote whole and unmodified, byte for byte, including the decimal-string satoshi values, together with a 2FA code (totpCode). The amount, the bank account, the fee and the whole quoted price are not sent as fields, they are taken from approvedQuote.terms, whose signature the platform verifies before using them, so what executes is the price that was shown, and the feeSummary in the answer is rebuilt from those same terms. Fails with quote_not_found when the terms do not verify for this merchant, a forged signature, an edited term and terms issued to someone else all answer the same way, and quote_expired when terms.expiresAt has passed, in which case the merchant approves a fresh quote; the platform never re-prices silently. Returns the created off-ramp with the feeSummary that was executed. When travelRuleRequired is true the off-ramp starts in awaiting_travel_rule.
Authorization: merchantAuth
Body required
application/jsonapprovedQuoteobjectrequiredThe quote the merchant approved, signed by the platform. Store it exactly as received and send it back to offramp/create whole, the amount, the bank account and the fee are read from it rather than from request fields, and any edit invalidates the signature.
totpCodestringrequiredstringResponses
idstringrequired3fa85f64-5717-4562-b3fc-2c963f66afa6resultobjectrequiredcurl -X POST "$BASE_URL/merchant/custodial/offramp/create" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{
"approvedQuote": {
"terms": {
"merchantId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"quoteId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"amountSats": "string",
"feeSats": "string",
"netSats": "string",
"bankAccountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"destinationAmount": "string",
"destinationAsset": "string",
"exchangeRate": "string",
"expiresAt": 1718012345,
"travelRuleWillBeRequired": true
},
"signature": "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b"
},
"totpCode": "string"
}'{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"result": {
"$case": "success",
"success": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"status": "pending",
"bankAccountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"amountSats": "string",
"feeSummary": {
"amountSats": "string",
"feeSats": "string",
"netSats": "string",
"destinationAmount": "string",
"destinationAsset": "string",
"exchangeRate": "string",
"feeBreakdown": [
{
"type": "withdraw",
"code": "string",
"asset": "string",
"amount": "string",
"percentage": 0
}
]
},
"travelRuleRequired": true,
"createdAt": 1718012345
}
}
}List off-ramps
https://api.gobtcpay.com/public/api/v1.2/merchant/custodial/offramp/listLists the merchant's off-ramp operations, newest first. Optionally filter by status and an inclusive from / to unix-second range; limit / skip paginate. Returns items and totalCount.
Authorization: merchantAuth
Body required
application/jsonstatusenumCurrent status. Payments: initiated / detected / paid / failed / expired / canceled / cleared. Payment transactions: success / failed / pending / cleared.
pendingawaiting_travel_rulesubmittingprocessingon_holdcompletedfailedcanceledpendingfromintegerUnixTime (seconds since epoch)
1718012345tointegerUnixTime (seconds since epoch)
1718012345limitnumber20skipnumber0Responses
idstringrequired3fa85f64-5717-4562-b3fc-2c963f66afa6resultobjectrequiredcurl -X POST "$BASE_URL/merchant/custodial/offramp/list" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{
"status": "pending",
"from": 1718012345,
"to": 1718012345,
"limit": 20,
"skip": 0
}'{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"result": {
"$case": "success",
"success": {
"items": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"status": "pending",
"bankAccountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"bankAccountLabel": "Front desk",
"routingNumberLast4": "string",
"accountNumberLast4": "string",
"accountType": "string",
"amountSats": "string",
"quotedFeeSats": null,
"quotedNetSats": null,
"quotedDestinationAmount": "string",
"quotedExchangeRate": "string",
"feeSats": null,
"netSats": null,
"destinationAmount": "string",
"exchangeRate": "string",
"feeBreakdown": [],
"failureReason": "insufficient_balance",
"travelRuleRequired": true,
"createdAt": 1718012345,
"submittedAt": 1718012345,
"completedAt": 1718012345
}
],
"totalCount": 0
}
}
}Get off-ramp
https://api.gobtcpay.com/public/api/v1.2/merchant/custodial/offramp/getReturns the full off-ramp record for offrampId, including quoted vs. settled fee figures, exchange rate, USD destination amount, feeBreakdown, failureReason, and lifecycle timestamps.
Authorization: merchantAuth
Body required
application/jsonofframpIdstringrequired3fa85f64-5717-4562-b3fc-2c963f66afa6Responses
idstringrequired3fa85f64-5717-4562-b3fc-2c963f66afa6resultobjectrequiredcurl -X POST "$BASE_URL/merchant/custodial/offramp/get" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{
"offrampId": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}'{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"result": {
"$case": "success",
"success": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"status": "pending",
"bankAccountId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"bankAccountLabel": "Front desk",
"routingNumberLast4": "string",
"accountNumberLast4": "string",
"accountType": "string",
"amountSats": "string",
"quotedFeeSats": "string",
"quotedNetSats": "string",
"quotedDestinationAmount": "string",
"quotedExchangeRate": "string",
"feeSats": "string",
"netSats": "string",
"destinationAmount": "string",
"exchangeRate": "string",
"feeBreakdown": [
{
"type": "withdraw",
"code": "string",
"asset": "string",
"amount": "string",
"percentage": 0
}
],
"failureReason": "insufficient_balance",
"travelRuleRequired": true,
"createdAt": 1718012345,
"submittedAt": 1718012345,
"completedAt": 1718012345
}
}
}Cancel off-ramp
https://api.gobtcpay.com/public/api/v1.2/merchant/custodial/offramp/cancelCancels the off-ramp identified by offrampId while it has not yet been submitted to Uphold. Fails with offramp_already_submitted once it has left the platform. Returns { ok }.
Authorization: merchantAuth
Body required
application/jsonofframpIdstringrequired3fa85f64-5717-4562-b3fc-2c963f66afa6Responses
idstringrequired3fa85f64-5717-4562-b3fc-2c963f66afa6resultobjectrequiredcurl -X POST "$BASE_URL/merchant/custodial/offramp/cancel" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{
"offrampId": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}'{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"result": {
"$case": "success",
"success": {
"ok": true
}
}
}Open Travel Rule session
https://api.gobtcpay.com/public/api/v1.2/merchant/custodial/offramp/travel-rule/sessionOpens an Uphold Travel Rule widget session for the off-ramp identified by offrampId, bound to its quote. Returns the session Uphold issued, relayed verbatim: flow, the url the widget mounts, a short-lived token and provider-specific data forwarded unchanged. Mount the widget with it and let the merchant complete the form there, the personal details go to the provider, never to this API. There is no expiry to compare against: the session is short-lived and single-use, so call this again to replace a stale one.
Authorization: merchantAuth
Body required
application/jsonofframpIdstringrequired3fa85f64-5717-4562-b3fc-2c963f66afa6Responses
idstringrequired3fa85f64-5717-4562-b3fc-2c963f66afa6resultobjectrequiredcurl -X POST "$BASE_URL/merchant/custodial/offramp/travel-rule/session" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{
"offrampId": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}'{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"result": {
"$case": "success",
"success": {
"flow": "withdrawal-form",
"url": "string",
"token": "eyJhbGciOiJFUzI1NiIsImtpZCI6ImFjY2VzcyJ9.eyJwa2giOiIwMmExNjMzYyIsImNpZCI6IjNmYTg1ZjY0IiwiaWF0IjoxNzE4MDEyMzQ1fQ.MEQCIE3aGoQk0vY9c5kq",
"data": "string"
}
}
}Check Travel Rule requirement
https://api.gobtcpay.com/public/api/v1.2/merchant/custodial/offramp/travel-rule/submitAsks whether the Travel Rule requirement on offrampId has been closed, after the merchant has finished in the widget. Takes nothing else: the platform collects no personal data and forwards none, so there is no payload to send and no session to identify, it reads the quote from the off-ramp and asks Uphold for the status of the request for information on it. Returns released, whether the off-ramp left awaiting_travel_rule and is executing again, and the status behind it: cleared (closed, released), pending (evidence still settling, wait and call again, the same session stands), failed (rejected, open a new session and redo), expired (lapsed, also a new session), unchecked (no request for information, or Uphold could not be asked, released anyway, since not knowing is not a refusal). Branch on released; show status. Safe to call repeatedly.
Authorization: merchantAuth
Body required
application/jsonofframpIdstringrequired3fa85f64-5717-4562-b3fc-2c963f66afa6Responses
idstringrequired3fa85f64-5717-4562-b3fc-2c963f66afa6resultobjectrequiredcurl -X POST "$BASE_URL/merchant/custodial/offramp/travel-rule/submit" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{
"offrampId": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}'{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"result": {
"$case": "success",
"success": {
"status": "cleared",
"released": true
}
}
}