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

POSThttps://api.gobtcpay.com/public/api/v1.2/merchant/custodial/offramp/quote

Prices 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/json
bankAccountIdstringrequired
Example:3fa85f64-5717-4562-b3fc-2c963f66afa6
amountSatsstring | integerrequired

Amount in satoshis.

Responses

idstringrequired
Example:3fa85f64-5717-4562-b3fc-2c963f66afa6
resultobjectrequired
POST /merchant/custodial/offramp/quote
cURL
curl -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"
}'
200 Successful response
JSON
{
  "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

POSThttps://api.gobtcpay.com/public/api/v1.2/merchant/custodial/offramp/create

Executes 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/json
approvedQuoteobjectrequired

The 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.

totpCodestringrequired
Example:string

Responses

idstringrequired
Example:3fa85f64-5717-4562-b3fc-2c963f66afa6
resultobjectrequired
POST /merchant/custodial/offramp/create
cURL
curl -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"
}'
200 Successful response
JSON
{
  "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

POSThttps://api.gobtcpay.com/public/api/v1.2/merchant/custodial/offramp/list

Lists 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/json
statusenum

Current status. Payments: initiated / detected / paid / failed / expired / canceled / cleared. Payment transactions: success / failed / pending / cleared.

pendingawaiting_travel_rulesubmittingprocessingon_holdcompletedfailedcanceled
Example:pending
frominteger

UnixTime (seconds since epoch)

Example:1718012345
tointeger

UnixTime (seconds since epoch)

Example:1718012345
limitnumber
Example:20
skipnumber
Example:0

Responses

idstringrequired
Example:3fa85f64-5717-4562-b3fc-2c963f66afa6
resultobjectrequired
POST /merchant/custodial/offramp/list
cURL
curl -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
}'
200 Successful response
JSON
{
  "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

POSThttps://api.gobtcpay.com/public/api/v1.2/merchant/custodial/offramp/get

Returns 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/json
offrampIdstringrequired
Example:3fa85f64-5717-4562-b3fc-2c963f66afa6

Responses

idstringrequired
Example:3fa85f64-5717-4562-b3fc-2c963f66afa6
resultobjectrequired
POST /merchant/custodial/offramp/get
cURL
curl -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"
}'
200 Successful response
JSON
{
  "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

POSThttps://api.gobtcpay.com/public/api/v1.2/merchant/custodial/offramp/cancel

Cancels 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/json
offrampIdstringrequired
Example:3fa85f64-5717-4562-b3fc-2c963f66afa6

Responses

idstringrequired
Example:3fa85f64-5717-4562-b3fc-2c963f66afa6
resultobjectrequired
POST /merchant/custodial/offramp/cancel
cURL
curl -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"
}'
200 Successful response
JSON
{
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "result": {
    "$case": "success",
    "success": {
      "ok": true
    }
  }
}

Open Travel Rule session

POSThttps://api.gobtcpay.com/public/api/v1.2/merchant/custodial/offramp/travel-rule/session

Opens 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/json
offrampIdstringrequired
Example:3fa85f64-5717-4562-b3fc-2c963f66afa6

Responses

idstringrequired
Example:3fa85f64-5717-4562-b3fc-2c963f66afa6
resultobjectrequired
POST /merchant/custodial/offramp/travel-rule/session
cURL
curl -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"
}'
200 Successful response
JSON
{
  "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

POSThttps://api.gobtcpay.com/public/api/v1.2/merchant/custodial/offramp/travel-rule/submit

Asks 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/json
offrampIdstringrequired
Example:3fa85f64-5717-4562-b3fc-2c963f66afa6

Responses

idstringrequired
Example:3fa85f64-5717-4562-b3fc-2c963f66afa6
resultobjectrequired
POST /merchant/custodial/offramp/travel-rule/submit
cURL
curl -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"
}'
200 Successful response
JSON
{
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "result": {
    "$case": "success",
    "success": {
      "status": "cleared",
      "released": true
    }
  }
}