This python script uses the XMR.to API to enable you to convert XMR to BTC instantly via the CLI.
It was written for and tested on Tails. On Whonix you may need to sudo apt install python3-pip && pip3 install requests first.


Copy the script below and save it to a new file named xmrto.py.
Open the terminal in the folder and run torsocks python3 xmrto.py

Display indicative rate and limits:
torsocks python3 xmrto.py rates

View the status of an existing trade:
torsocks python3 xmrto.py view
torsocks python3 xmrto.py view --id xmrto-xXxXxX

Exchange XMR to BTC:
torsocks python3 xmrto.py exchange

Example dialog:

amnesia@amnesia:~$ torsocks python3 xmrto.py exchange
Indicative rate: 0.016311 BTC/XMR
Minimum: 0.001 BTC - Maximum 20.0 BTC
Orders up to 0.1 BTC will be sent out instantly.

Destination BTC address: bxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Choose option:
1. I want to specify the amount of Bitcoin I want to receive from XMR.to
2. I want to specify the amount of Monero I will send to XMR.to
Enter option number: 1
Amount of BTC to receive from XMR.to: 0.001

XMR.to will send 0.001 BTC to bxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Your unique identifier for this transaction is: xmrto-xXxXxX

Would you like the script to automatically update the status of the order?
[yes/no]: y
Requesting order status...

Please send x.xxxx XMR to

CLI command:

This transaction will expire at XXXX-XX-XXTxx:xx:xxZ
The script will automatically refresh the order status every 30 seconds.
You can exit the script prematurely with CTRL + C

Bitcoin payment sent.
TX id: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx


NOTE: Review the code yourself. It comes with ABSOLUTELY NO WARRANTY.

import json
import argparse
from sys import argv
import requests
from time import sleep

XMR_TO_API_URL = "https://xmr.to/api/v3/xmr2btc/"

def create_order(args):
    if not args.address:
        args.address = input("Destination BTC address: ").strip()

    if not args.currency:
        print("\nChoose option:")
        print("1. I want to specify the amount of Bitcoin I will receive from XMR.to")
        print("2. I want to specify the amount of Monero I will send to XMR.to")
        while True:
            option = input("Enter option number: ").strip().replace(".","")
            if option not in ["1", "2"]:
        args.currency = "BTC" if option == "1" else "XMR"

    if not args.amount:
        if args.currency == "BTC":
            args.amount = input("Amount of BTC to receive from XMR.to: ")
            args.amount = input("Amount of XMR to send to XMR.to: ")

    res = requests.post(XMR_TO_API_URL + 'order_create/', 
        data={'amount': args.amount,
              'amount_currency': args.currency,
              'btc_dest_address': args.address})

    if res.status_code == 201:
        order_data = res.json()
        print("\nXMR.to will send {} BTC to {}"
        print("Your unique identifier for this trade is: {}"

    return order_data['uuid']

def handle_failure(res):
    if res.status_code in [400, 403, 404, 503]:
            print("\nXMR.to error: {}".format(res.json()['error_msg']))
            print("\nUnknown error, please try again")
        print("\nUnknown error, please try again")

def exchange(args):

    args.id = create_order(args)

    if not args.autorefresh:
         args.autorefresh = input("\nWould you like the script to automatically update the status of the order?\n[yes/no]: " ).lower().strip() in ['y','yes']

    print("Requesting order status...")

    if args.autorefresh:
        last_status = None
        while True:
            order_status = get_status(args)

            new_status = (order_status['state'], 

            if new_status != last_status:
                last_status = new_status


def get_status(args):
    if not args.id:
        args.id = input("unique identifier: ")

    res = requests.post(XMR_TO_API_URL + 'order_status_query/',
        data={'uuid': args.id})

    if res.status_code != 200:

    return res.json()

def view_trade(args):
    order_status = get_status(args)

    state = order_status['state']

    if 'autorefresh' not in args:
        args.autorefresh = False

    if state == 'TO_BE_CREATED':
        print("Order is pending creation")
    if state == 'UNPAID':
        print("Please send {} XMR to\n{}".format(order_status['incoming_amount_total'], order_status['receiving_subaddress']))
        print("\nCLI command:\ntransfer {} {}\n".format(order_status['receiving_subaddress'], order_status['incoming_amount_total']))
        print("\nThis trade will expire at {}".format(order_status['expires_at']))
        if args.autorefresh:
            print("The script will automatically refresh the order status every 30 seconds.\nYou can exit the script prematurely with CTRL + C")
    elif state == 'UNDERPAID':
        print('The transaction was underpaid. You still need to send {} XMR to\n{}'.format(order_status['remaining_amount_incoming'], order_status['receiving_subaddress']))
    elif state == 'PAID_UNCONFIRMED':
        print('Order paid, waiting for enough confirmations')
        print('Number of confirmations remaining: {}'.format(order_status['incoming_num_confirmations_remaining']))
    elif state == 'PAID':
        print('Order paid and sufficiently confirmed. BTC will be sent shortly.')
        print('Contact support at support@xmr.to if this takes longer than expected')
    elif state == 'BTC_SENT':
        print('Bitcoin payment sent.\nTX id: {}'.format(order_status['payments'][0]['tx_id']))
        if args.autorefresh:
    elif state == 'TIMED_OUT':
        print('Order timed out before payment was complete. Contact support at support@xmr.to to request a refund.')
    elif state == 'NOT_FOUND':
        print("Order wasn't found in system (never existed or was purged)")

def display_rates(args):
    res = requests.get(XMR_TO_API_URL + 'order_parameter_query')

    if res.status_code != 200:

    order_params = res.json()

    print('Indicative rate: {} BTC/XMR'.format(order_params['price']))
    print('Minimum: {} BTC - Maximum {} BTC'.format(order_params['lower_limit'], order_params['upper_limit']))
    if order_params['zero_conf_enabled']:
        print('Orders up to {} BTC will be sent out instantly.'.format(order_params['zero_conf_max_amount']))
        print('Instant sending is currently disabled for all amounts.')

if __name__ == "__main__":
    parser = argparse.ArgumentParser(prog='python3 xmrto.py')
    subparsers = parser.add_subparsers()

    parser_rates = subparsers.add_parser('rates', help= 'Display indicative rate and limits')

    parser_view = subparsers.add_parser('view', help='Fetch an existing trade')
    parser_view.add_argument('--id', type=str, help='ID of trade to lookup')

    parser_exchange = subparsers.add_parser('exchange', help='Exchange XMR to BTC')
    parser_exchange.add_argument('--amount', type=float, help='Amount of BTC you want to receive, or amount of XMR you want to convert')
    parser_exchange.add_argument('--currency', type=str, help='Currency of amount')
    parser_exchange.add_argument('--address', type=str, help='BTC destination address')
    parser_exchange.add_argument('--autorefresh', action='store_true')

    args = parser.parse_args()
    if argv[1:]:
        print("""Usage:\n\nDisplay indicative rate and limits:
torsocks python3 xmrto.py rates\n
View the status of an existing trade:
torsocks python3 xmrto.py view\n
Exchange XMR to BTC:
torsocks python3 xmrto.py exchange""")

2020-09-10 updated for api v3
2019-11-27 minor error handling fix