Crypto-as-a-Service: Withdraw Crypto On Chain from Go Account
Build, authenticate, and send crypto withdrawals from your Go Account to on-chain addresses. See the Guide.
-
You build an unsigned transaction to withdraw assets from your Go Account to an on-chain address. Crypto asset IDs in Go Accounts always start with "ofc" (such as ofctbtc for testnet Bitcoin).
-
You authenticate the transaction using your Go Account passphrase. To ensure your passphrase isn't passed over the Internet, you must use BitGo Express in external-signing mode, the JavaScript SDK, or Java. This step signs the payload and returns a signature.
-
You send the half-signed transaction to BitGo. BitGo uses the data you provided to construct the final transaction. After video verification, BitGo operators sign and broadcast the transaction.
-
If you configure an approval requirement for withdrawals, another admin must approve the transaction. You can't approve your own transactions.
// 1. Build Transaction
export COIN="<CRYPTO_ASSET_ID>"
export WALLET_ID="<YOUR_WALLET_ID>"
export SERVICE_USER_TOKEN="<SERVICE_USER_ACCESS_TOKEN>"
export AMOUNT="<AMOUNT_IN_BASE_UNITS>"
export ADDRESS="<DESTINATION_ADDRESS>"
curl -X POST \
https://app.bitgo-test.com/api/v2/$COIN/wallet/$WALLET_ID/tx/build \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $SERVICE_USER_TOKEN" \
-d '{
"recipients": [
{
"amount": "'"$AMOUNT"'",
"address": "'"$ADDRESS"'"
}
]
}'
// 2. Authenticate Transaction
export BITGO_EXPRESS_HOST="<YOUR_LOCAL_HOST>"
export WALLET_PASSPHRASE="<YOUR_GO_ACCOUNT_PASSPHRASE>"
curl -X POST \
http://$BITGO_EXPRESS_HOST/api/v2/ofc/signPayload \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $SERVICE_USER_TOKEN" \
-d '{
"walletId": "'"$WALLET_ID"'",
"walletPassphrase": "'"$WALLET_PASSPHRASE"'",
"payload": "{\"coin\":\"ofctbtc\",\"recipients\":[{\"address\":\"tb1q5mwcr2749nnm5c5d6rzx5h92l2u9ex5jzygzd39vfv9qerj5fmjqwlgy4t\",\"amount\":\"190000\"}],\"fromAccount\":\"67ab85b5190bf872e84da2b6e527d9f3\",\"nonce\":\"77d91a11-a7fa-4ad7-91e5-f054328717f9\",\"timestamp\":\"2025-06-26T15:02:06.924Z\",\"feeString\":\"0\",\"shortCircuitBlockchainTransfer\":false,\"isIntraJXTransfer\":false}"
}'
// 3. Send Transaction
curl -X POST \
https://app.bitgo-test.com/api/v2/$COIN/wallet/$WALLET_ID/tx/send \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $SERVICE_USER_TOKEN" \
-d '{
"recipients": [
{
"address": "'"$ADDRESS"'",
"amount": "'"$AMOUNT"'"
}
],
"halfSigned": {
"payload": {
"coin": "ofctbtc",
"recipients": [
{
"address": "'"$ADDRESS"'",
"amount": "'"$AMOUNT"'"
}
],
"fromAccount": "<WALLET_ID>",
"nonce": "f4af25d0-a037-4afc-8ce6-d7f158d826d1",
"timestamp": "2021-09-10T22:45:20.777Z",
"idfSignedTimestamp": "2021-09-10T22:45:20.113Z",
"idfVersion": 1,
"idfUserId": "<USER_ID>",
"feeString": "1000"
},
"signature": "<SIGNATURE>"
}
}'
// 4. Approve Transaction (Optional)
export APPROVAL_ID="<APPROVAL_ID>"
export OTP="<OTP>"
curl -X POST \
http://api/v2/$COIN/pendingapprovals/$APPROVAL_ID \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $SERVICE_USER_TOKEN" \
-d '{
"state": "approved",
"otp": "'"$OTP"'"
}'
// 1. Build Transaction
let params = {
recipients: [
{
amount: 1000,
address: '2NFfxvXpAWjKng7enFougtvtxxCJ2hQEMo4',
},
],
};
wallet.prebuildTransaction(params).then(function (transaction) {
console.dir(transaction);
});
// 2. Authenticate Transaction
const wallet = await bitgo.coin('ofc').wallets().get({ id: goAccountId });
if (wallet === undefined) {
throw new Error(`Could not find OFC wallet ${goAccountId}`);
}
const walletPassphrase = "<YOUR_WALLET_PASSPHRASE>";
const tradingAccount = wallet.toTradingAccount();
const stringifiedPayload = JSON.stringify(<payload_returned_by_tx/build_api>);
const signature = await tradingAccount.signPayload({
payload: stringifiedPayload,
walletPassphrase,
});
// 3. Send Transaction
// Use the REST API for this step (see cURL tab)
// 4. Approve Transaction (Optional)
const baseCoin = this.bitgoSDK.coin(initialPendingApproval.coin);
const pendingApproval = await baseCoin.pendingApprovals().get({ id: initialPendingApproval.id });
const result = await pendingApproval.approve(params);
// 1. Build Transaction
// Use the cURL tab for this step
// 2. Authenticate Transaction
import javax.crypto.*;
import javax.crypto.spec.*;
import java.security.*;
import java.security.spec.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
import com.google.gson.*;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.Sha256Hash;
import org.bitcoinj.crypto.DeterministicKey;
public class SignPayload {
public static void main(String[] args) throws Exception {
System.out.println("=== Sign Payload for Go Account Withdrawal ===\n");
// Configuration - replace with your values
String walletPassphrase = "your_wallet_passphrase";
String encryptedPrivateKey = "{\"iv\":...";
String payloadToSign = "{\"coin\":...";
// Step 1: Decrypt private key
System.out.println("Step 1: Decrypt Private Key");
System.out.println("-".repeat(50));
Gson gson = new Gson();
JsonObject data = gson.fromJson(encryptedPrivateKey, JsonObject.class);
byte[] salt = Base64.getDecoder().decode(data.get("salt").getAsString());
byte[] iv = Base64.getDecoder().decode(data.get("iv").getAsString());
byte[] ciphertextWithTag = Base64.getDecoder().decode(data.get("ct").getAsString());
int iterations = data.get("iter").getAsInt();
int keySize = data.get("ks").getAsInt();
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
KeySpec spec = new PBEKeySpec(walletPassphrase.toCharArray(), salt, iterations, keySize);
SecretKey tmp = factory.generateSecret(spec);
SecretKeySpec secretKey = new SecretKeySpec(tmp.getEncoded(), "AES");
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec gcmSpec = new GCMParameterSpec(128, iv);
cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmSpec);
byte[] decrypted = cipher.doFinal(ciphertextWithTag);
String decryptedPrv = new String(decrypted, StandardCharsets.UTF_8);
System.out.println("✓ Private key decrypted");
System.out.println();
// Step 2: Sign payload
System.out.println("Step 2: Sign Payload");
System.out.println("-".repeat(50));
DeterministicKey key = DeterministicKey.deserializeB58(decryptedPrv, org.bitcoinj.params.MainNetParams.get());
ECKey ecKey = ECKey.fromPrivate(key.getPrivKeyBytes());
// Bitcoin message format
String messagePrefix = "\u0018Bitcoin Signed Message:\n";
byte[] payloadBytes = payloadToSign.getBytes(StandardCharsets.UTF_8);
byte[] prefixBytes = messagePrefix.getBytes(StandardCharsets.UTF_8);
byte[] lengthVarint = encodeVarint(payloadBytes.length);
byte[] messageToSign = new byte[prefixBytes.length + lengthVarint.length + payloadBytes.length];
System.arraycopy(prefixBytes, 0, messageToSign, 0, prefixBytes.length);
System.arraycopy(lengthVarint, 0, messageToSign, prefixBytes.length, lengthVarint.length);
System.arraycopy(payloadBytes, 0, messageToSign, prefixBytes.length + lengthVarint.length, payloadBytes.length);
// Double SHA-256 hash
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] firstHash = digest.digest(messageToSign);
byte[] messageHash = digest.digest(firstHash);
// Sign
ECKey.ECDSASignature signature = ecKey.sign(Sha256Hash.wrap(messageHash));
// Create 65-byte recoverable signature
byte[] result = new byte[65];
// Calculate recovery ID
int recoveryId = -1;
for (int i = 0; i < 4; i++) {
try {
ECKey recovered = ECKey.recoverFromSignature(i, signature, Sha256Hash.wrap(messageHash), true);
if (recovered != null && Arrays.equals(recovered.getPubKey(), ecKey.getPubKey())) {
recoveryId = i;
break;
}
} catch (Exception e) {
// Continue trying
}
}
if (recoveryId == -1) {
throw new RuntimeException("Could not determine signature recovery ID");
}
result[0] = (byte) (31 + recoveryId);
copyBigIntegerTo32Bytes(signature.r.toByteArray(), result, 1);
copyBigIntegerTo32Bytes(signature.s.toByteArray(), result, 33);
String hexSignature = bytesToHex(result);
System.out.println("✓ Payload signed");
System.out.println();
System.out.println("Signature (hex):");
System.out.println(hexSignature);
System.out.println();
System.out.println("✓ Use this signature with the /tx/send endpoint");
}
private static byte[] encodeVarint(long value) {
if (value < 253) {
return new byte[] { (byte) value };
} else if (value <= 0xFFFF) {
byte[] result = new byte[3];
result[0] = (byte) 0xFD;
result[1] = (byte) (value & 0xFF);
result[2] = (byte) ((value >> 8) & 0xFF);
return result;
}
throw new RuntimeException("Payload too large");
}
private static void copyBigIntegerTo32Bytes(byte[] source, byte[] dest, int destOffset) {
int sourceOffset = 0;
int sourceLength = source.length;
if (sourceLength > 32) {
sourceOffset = 1;
sourceLength = 32;
}
int destStart = destOffset + (32 - sourceLength);
System.arraycopy(source, sourceOffset, dest, destStart, sourceLength);
}
private static String bytesToHex(byte[] bytes) {
StringBuilder result = new StringBuilder();
for (byte b : bytes) {
result.append(String.format("%02x", b));
}
return result.toString();
}
}
// 3. Send Transaction
// Use the cURL tab for this step
// 4. Approve Transaction (Optional)
// Use the cURL tab for this step
// 1. Build Transaction Response
{
"payload": "{\"coin\":\"ofctbtc\",\"recipients\":[{\"address\":\"tb1q5mwcr2749nnm5c5d6rzx5h92l2u9ex5jzygzd39vfv9qerj5fmjqwlgy4t\",\"amount\":\"1\"}],\"fromAccount\":\"62c5b1de8a0c5200071c9a603bdbadc5\",\"nonce\":\"d8e5d930-f827-42b1-ad85-40544dad1be6\",\"timestamp\":\"2025-06-25T17:45:56.394Z\",\"feeString\":\"0\",\"shortCircuitBlockchainTransfer\":false,\"isIntraJXTransfer\":false}",
"feeInfo": { "feeString": "0" },
"coin": "ofc",
"token": "ofctbtc"
}
// 2. Authenticate Transaction Response
{
"coin": "ofctbtc",
"recipients": [
{
"address": "tb1q5mwcr2749nnm5c5d6rzx5h92l2u9ex5jzygzd39vfv9qerj5fmjqwlgy4t",
"amount": "1"
}
],
"fromAccount": "62c5b1de8a0c5200071c9a603bdbadc5",
"nonce": "77d91a11-a7fa-4ad7-91e5-f054328717f9",
"timestamp": "2025-06-26T15:02:06.924Z",
"feeString": "0",
"shortCircuitBlockchainTransfer": false,
"isIntraJXTransfer": false,
"payload": "{\"coin\":\"ofctbtc\",\"recipients\":[{\"address\":\"tb1q5mwcr2749nnm5c5d6rzx5h92l2u9ex5jzygzd39vfv9qerj5fmjqwlgy4t\",\"amount\":\"1\"}],\"fromAccount\":\"62c5b1de8a0c5200071c9a603bdbadc5\",\"nonce\":\"77d91a11-a7fa-4ad7-91e5-f054328717f9\",\"timestamp\":\"2025-06-26T15:02:06.924Z\",\"feeString\":\"0\",\"shortCircuitBlockchainTransfer\":false,\"isIntraJXTransfer\":false}",
"signature": "20dda1e9558adb297f69020f94cbf4955fabec73478506347dbb5ac8e73b506fc908bbc48bde75b339b99f5de808ed34b2017055c9df198fd1f8b4e524899f9583"
}
// 3. Send Transaction Response
{
"transfer": {
"id": "<ID>",
"coin": "ofctbtc",
"wallet": "<WALLET_ID>",
"walletType": "trading",
"enterprise": "603ca88c44f82002560378421dc7a191",
"txid": "ddd6875812fa59f899a68059ff303bfa6d39fc350ee687c407383d0444b43d2b",
"height": 999999999,
"heightId": "999999999-613be001f3de560006842d52251d5d49",
"date": "2021-09-10T22:45:21.342Z",
"type": "send",
"value": -10000,
"valueString": "-10000",
"baseValue": -10000,
"baseValueString": "-10000",
"feeString": "0",
"payGoFee": 0,
"payGoFeeString": "0",
"usd": -100,
"usdRate": 1,
"state": "signed",
"instant": false,
"isReward": false,
"isFee": false,
"tags": [
"603ca8c01b83cb000656311787dab4cd",
"603ca88c44f82002560378421dc7a191"
],
"history": [
{
"date": "2021-09-10T22:45:21.340Z",
"action": "signed"
},
{
"date": "2021-09-10T22:45:21.216Z",
"user": "5e4ed695ae2e542100336fb16e586019",
"action": "created"
}
],
"signedDate": "2021-09-10T22:45:21.340Z",
"entries": [
{
"address": "address",
"wallet": "603ca8c01b83cb000656311787dab4cd",
"value": -10000,
"valueString": "-10000"
},
{
"address": "bf6cc5252438dc85",
"value": 10000,
"valueString": "10000"
}
],
"signedTime": "2021-09-10T22:45:21.340Z",
"createdTime": "2021-09-10T22:45:21.216Z"
},
"txid": "ddd6875812fa59f899a68059ff303bfa6d39fc350ee687c407383d0444b43d2b",
"tx": {
"transaction": {
"coin": "ofctbtc",
"recipients": [
{
"address": "<DESTINATION_ADDRESS>",
"amount": "10000"
}
],
"fromAccount": "<WALLET_ID>",
"nonce": "f4af25d0-a037-4afc-8ce6-d7f158d826d1",
"timestamp": "2021-09-10T22:45:20.777Z",
"idfSignedTimestamp": "2021-09-10T22:45:20.113Z",
"idfVersion": 1,
"idfUserId": "<USER_ID>",
"feeString": "0"
},
"signature": "<SIGNATURE>",
"idfVersion": 1,
"idfSignedTimestamp": "2021-09-10T22:45:20.113Z",
"idfUserId": "<USER_ID>"
},
"status": "signed"
}
// 4. Approve Transaction (Optional) Response
{
"id": "655686880765186f0b3e9e88e1bdd0f4",
"coin": "tbtc",
"wallet": "6553e933288be490293ae748efafeaaf",
"enterprise": "62c5ae8174ac860007aff138a2d74df7",
"creator": "62ab90e06dfda30007974f0a52a12995",
"createDate": "2023-11-16T21:15:52.703Z",
"info": {
"type": "transactionRequest",
"transactionRequest": {
"requestedAmount": "10000",
"fee": 45242,
"sourceWallet": "6553e933288be490293ae748efafeaaf",
"policyUniqueId": "6553e933288be490293ae753",
"recipients": [
{
"address": "2N3sBpM1RnWRMXnEVoUWnM7xtYzL756JE2Q",
"amount": "10000",
"_id": "655686880765186f0b3e9e8a"
}
],
"buildParams": {
"recipients": [
{
"address": "2N3sBpM1RnWRMXnEVoUWnM7xtYzL756JE2Q",
"amount": "10000"
}
]
},
"coinSpecific": {
"tbtc": {
"txHex": "01000000000101eac5e7d68acfc2349672fb99094e79c2006e5fd663475c8f1eb6f29095970b740100000000ffffffff02102700000000000017a914747e6b7f5db53794b0fc01d95878e0f9b6db96738746c1020000000000220020efb9deaabeab5d62cecc363f24cd6deafcdd14d93d8734f7f01ed3953e732a640500483045022100cd6c221e4cefbb51aa82087d86e7d089b2473eb21ae809dba89eb9f13c4caf19022000f3b7f1b7fec2dc49cbfce35baa69ca37ab9ee8e51c779cde5b98a653f3e142010000695221029bde55661e4f359cf9ca2d1846c235e752f760ed6d65e2018f821253e78b3c722103f2b4a813ab79e4fb46edf3a6a87fb154a85b45810158e949f41a3fce3c9bf574210399a6d766f6d3a3843f8479446ee766b5745dc15c24d1db5149214d27987bd29453ae00000000"
}
},
"validTransaction": "01000000000101eac5e7d68acfc2349672fb99094e79c2006e5fd663475c8f1eb6f29095970b740100000000ffffffff02102700000000000017a914747e6b7f5db53794b0fc01d95878e0f9b6db96738746c1020000000000220020efb9deaabeab5d62cecc363f24cd6deafcdd14d93d8734f7f01ed3953e732a640400483045022100cd6c221e4cefbb51aa82087d86e7d089b2473eb21ae809dba89eb9f13c4caf19022000f3b7f1b7fec2dc49cbfce35baa69ca37ab9ee8e51c779cde5b98a653f3e14201473044022002b246c43722fec49858caf6b0605a0d01a6b6c13c964b84bf272604fd3951780220324ee44ba3ce8febb154c317e688a5f6ed410ad8e2bd77c5678bf1f7f3eb45f601695221029bde55661e4f359cf9ca2d1846c235e752f760ed6d65e2018f821253e78b3c722103f2b4a813ab79e4fb46edf3a6a87fb154a85b45810158e949f41a3fce3c9bf574210399a6d766f6d3a3843f8479446ee766b5745dc15c24d1db5149214d27987bd29453ae00000000",
"validTransactionHash": "ff40ccd5c8ba75ffdce27d8584b6e8ee625cb3f71f7cbb6d072a445a97c2c8c3"
}
},
"state": "approved",
"scope": "wallet",
"userIds": [
"62ab90e06dfda30007974f0a52a12995",
"627ff9325a5c1b0007c05a40d15e1522"
],
"approvalsRequired": 1,
"singleRunResults": [
{
"ruleId": "Custody Enterprise Transaction ID Verification",
"triggered": false,
"_id": "655686880765186f0b3e9e89"
}
],
"resolvers": [
{
"user": "627ff9325a5c1b0007c05a40d15e1522",
"date": "2023-11-16T21:33:24.644Z",
"resolutionType": "pending"
}
]
}