# OANDA v20 API in Python: token to first call

Independent and unofficial. Not affiliated with or endorsed by any broker or platform named here. Educational only: nothing here is advice, and nothing here recommends a broker or a trade.

This is the markdown version of https://openbrokerapi.com/brokers/oanda/quickstart, written from the same records as that page.

Risk warning: CFDs and spread bets are complex instruments and come with a high risk of losing money rapidly due to leverage. You can lose money on these products, and each provider publishes its own loss figure under FCA rules. You should consider whether you understand how they work and whether you can afford to take the high risk of losing your money. Automated and API trading can amplify losses through bugs, connectivity failures and runaway orders.

Make your first OANDA v20 API call in Python, JavaScript or curl: personal token, practice host and account summary, using placeholder credentials only.

- Checked: 2026-09-25

Unverified: the code on this page was written from the documentation and has not yet been run on a demo account.

Demo accounts only. Credentials are read from environment variables on your own machine. No field on this site asks for a key, a username or a password.

## Steps

### 1. Generate a personal access token

Log in to the Account Management Portal on fxTrade, select Manage API Access under My Services, agree to the licence and generate your token. Not verified: whether one token works on both hosts. OANDA's pages as read on 12 Oct 2026 do not say. The development guide pairs the practice host with your fxTrade Practice account and the live host with your fxTrade account, and the authentication page says a token is unique to an OANDA account. This page said until 12 Oct 2026 that each environment needs its own token and that the wrong one fails with Insufficient authorization to perform request: that is not on the pages cited, so it is withdrawn as a statement of fact. Generate the token from the account you mean to call. The samples below all call the practice host, so generate the practice token.

Status: verified. Source: [developer.oanda.com](https://developer.oanda.com/rest-live-v20/introduction/), checked 2026-09-25.

### 2. Pick your host

Practice REST: https://api-fxpractice.oanda.com. Live REST: https://api-fxtrade.oanda.com. Streaming lives on stream-fxpractice.oanda.com and stream-fxtrade.oanda.com.

Status: verified. Source: [developer.oanda.com](https://developer.oanda.com/rest-live-v20/development-guide/), checked 2026-09-25.

### 3. Send the token as a Bearer header

Every request carries Authorization: Bearer followed by your token, which the code reads from the OANDA_TOKEN environment variable. No session login and no CST headers, so there is less to go wrong. You do not need a wrapper library for this, and you should not reach for the most downloaded one: our library health page records oandapyV20 at 0.7.2 of August 2021, with no release since. The samples below use requests and fetch only.

Status: verified. Source: [developer.oanda.com](https://developer.oanda.com/rest-live-v20/authentication/), checked 2026-09-25.

### 4. First call: list your accounts

GET /v3/accounts returns every account the token can see. Note the account ID, you need it for everything else.

Python:

```python
# Set OANDA_TOKEN as an environment variable first. Never paste real values into code or into this site.
import os
import requests

BASE_URL = "https://api-fxpractice.oanda.com"
headers = {
    "Content-Type": "application/json",
    "Authorization": "Bearer " + os.environ["OANDA_TOKEN"],
}

resp = requests.get(BASE_URL + "/v3/accounts", headers=headers, timeout=10)
resp.raise_for_status()
print(resp.json())
```

JavaScript:

```javascript
// Set OANDA_TOKEN as an environment variable first. Never paste real values into code or into this site.
// Node 18 or later. Save as a .mjs file so top level await works.
const BASE_URL = "https://api-fxpractice.oanda.com";

const res = await fetch(`${BASE_URL}/v3/accounts`, {
  method: "GET",
  headers: {
    "Content-Type": "application/json",
    "Authorization": `Bearer ${process.env.OANDA_TOKEN}`
  }
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
console.log(await res.json());
```

curl:

```bash
# Set OANDA_TOKEN as an environment variable first. Never paste real values into code or into this site.
curl "https://api-fxpractice.oanda.com/v3/accounts" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $OANDA_TOKEN"
```

Status: verified. Source: [developer.oanda.com](https://developer.oanda.com/rest-live-v20/account-ep/), checked 2026-09-25.

### 5. Second call: account summary

GET /v3/accounts/{accountID}/summary gives NAV, balance, margin available and open position counts.

Python:

```python
# Set OANDA_TOKEN, OANDA_ACCOUNT_ID as environment variables first. Never paste real values into code or into this site.
import os
import requests

BASE_URL = "https://api-fxpractice.oanda.com"
ACCOUNT_ID = os.environ["OANDA_ACCOUNT_ID"]  # from the previous step
headers = {
    "Authorization": "Bearer " + os.environ["OANDA_TOKEN"],
}

resp = requests.get(f"{BASE_URL}/v3/accounts/{ACCOUNT_ID}/summary", headers=headers, timeout=10)
resp.raise_for_status()
print(resp.json())
```

JavaScript:

```javascript
// Set OANDA_TOKEN, OANDA_ACCOUNT_ID as environment variables first. Never paste real values into code or into this site.
// Node 18 or later. Save as a .mjs file so top level await works.
const BASE_URL = "https://api-fxpractice.oanda.com";
const ACCOUNT_ID = process.env.OANDA_ACCOUNT_ID; // from the previous step

const res = await fetch(`${BASE_URL}/v3/accounts/${ACCOUNT_ID}/summary`, {
  method: "GET",
  headers: {
    "Authorization": `Bearer ${process.env.OANDA_TOKEN}`
  }
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
console.log(await res.json());
```

curl:

```bash
# Set OANDA_TOKEN, OANDA_ACCOUNT_ID as environment variables first. Never paste real values into code or into this site.
curl "https://api-fxpractice.oanda.com/v3/accounts/$OANDA_ACCOUNT_ID/summary" \
  -H "Authorization: Bearer $OANDA_TOKEN"
```

Status: verified. Source: [developer.oanda.com](https://developer.oanda.com/rest-live-v20/account-ep/), checked 2026-09-25.

### 6. Mind the limits

OANDA states limits on two pages, in different units. The Best Practices page, linked below, states no more than 2 new connections per second and no more than 100 requests per second on a persistent connection. The development guide states 120 requests per second per IP address, with HTTP 429 above it; that figure is the headline row in the limits table below, with its own source and date. Neither figure replaces the other, so reuse your connection and stay under both. This site withdrew the 120 figure on 23 September 2026 and reinstated it on 24 September 2026; the sequence is on the changes page.

Status: verified. Source: [developer.oanda.com](https://developer.oanda.com/rest-live-v20/best-practices/), checked 2026-09-25.

### 7. From first call to first order

One Python file built from the OANDA endpoint records on this site, as the last step of this quickstart. It reads OANDA_TOKEN from the environment and OANDA_ACCOUNT_ID where set. GET /v3/accounts/{accountID}/openPositions (version v3) reads your open positions. POST /v3/accounts/{accountID}/orders (version v3) places one small demo order with every field the record marks required (order). GET /v3/accounts/{accountID}/orders/{orderSpecifier} (version v3) reads the confirmation or status. It sends at most one request every second, which stays under each request limit in the OANDA limits table below. It refuses to run against any host but https://api-fxpractice.oanda.com, and it never prints a token. Dry run is the default: save it as oanda_first_order.py, run python oanda_first_order.py, and it prints each request it would send with every secret shown as the name of its variable, reading no credential at all. Run python oanda_first_order.py send to send them to the demo host. The script has not been run against OANDA.

Python:

```python
#!/usr/bin/env python3
"""OANDA demo API: from first call to first order.

Unofficial example from Broker API Hub, not published by OANDA. Written from the OANDA endpoint records on
openbrokerapi.com and not run against OANDA: the quickstart page says so. Demo host only.

What it does, in order:
  1. proves the token with GET /v3/accounts (version v3)
  2. GET /v3/accounts/{accountID}/openPositions (version v3): reads your open positions
  3. POST /v3/accounts/{accountID}/orders (version v3): places one small demo order with every field the record marks required
  4. GET /v3/accounts/{accountID}/orders/{orderSpecifier} (version v3): reads the confirmation or status of that order

Dry run is the default: nothing is sent, no credential is read, and each request is printed as it
would go, with every secret shown as the name of the variable that holds it. Run it with the word
send as the first argument (python oanda_first_order.py send) to send the requests to the demo host.

Before a real send, set these environment variables on your own machine:
  OANDA_TOKEN        your personal access token for the practice account
  OANDA_ACCOUNT_ID   the account id from GET /v3/accounts
Never paste real values into this file or into any website.
Needs Python 3.8 or later and requests (pip install requests).
"""
import json
import os
import sys
import time

import requests

DEMO_HOST = "https://api-fxpractice.oanda.com"
BASE_URL = os.environ.get("OANDA_BASE_URL", DEMO_HOST).rstrip("/")
SEND = len(sys.argv) > 1 and sys.argv[1] == "send"
# Pacing, from this site's limits register for OANDA:
#   120/s per IP address, HTTP 429 above it (ip; checked 3 Oct 2026)
# One request every second stays under each of them.
MIN_GAP = 1

if BASE_URL != DEMO_HOST:
    sys.exit("Refusing to run: this script only talks to the demo host " + DEMO_HOST)

# The order. Every field the order record marks required is here, with the record's own words after it,
# and so is every field the vendor's page marks neither required nor optional (tagged not marked by the vendor).
# A value of "..." or 0 is a placeholder: set it before a real send, or the script refuses to send.
ORDER = {
    "order": {"type": "MARKET", "instrument": "...", "units": "1", "timeInForce": "FOK", "positionFill": "DEFAULT"},  # The order request. For a market order: type MARKET, instrument, units (positive to buy, negative to sell), timeInForce FOK, positionFill DEFAULT. (required)
}


def env(name):
    """A credential or setting. In a dry run the value is the variable's own name, so nothing secret is read or printed."""
    if not SEND:
        return "<" + name + ">"
    value = os.environ.get(name)
    if not value:
        sys.exit("Set the " + name + " environment variable first.")
    return value


# Session and credential headers live in this object's memory only: never printed, never saved.
http = requests.Session()
http.headers.update({
    "Authorization": "Bearer " + env("OANDA_TOKEN"),
    "Accept": "application/json",
})
last_call = 0.0


def request(method, path, version=None, body=None, params=None):
    """One request: printed in a dry run, paced and sent otherwise. Exits on an error reply, without printing a token."""
    global last_call
    headers = {}
    if version:
        headers["VERSION"] = version
    if not SEND:
        print("DRY RUN " + method + " " + BASE_URL + path)
        shown = dict(http.headers)
        shown.update(headers)
        print("  headers: " + json.dumps(shown, indent=4))
        if params:
            print("  query: " + json.dumps(params))
        if body is not None:
            print("  body: " + json.dumps(body, indent=4))
        return None
    wait = MIN_GAP - (time.monotonic() - last_call)
    if wait > 0:
        time.sleep(wait)
    last_call = time.monotonic()
    resp = http.request(method, BASE_URL + path, headers=headers, json=body, params=params, timeout=15)
    if resp.status_code >= 400:
        sys.exit("HTTP " + str(resp.status_code) + " from " + method + " " + path + ": " + resp.text[:300])
    print(method + " " + path + ": HTTP " + str(resp.status_code))
    return resp


def main():
    # GET /v3/accounts (version v3): List every account your token can access. Start here to find your accountID.
    resp = request("GET", "/v3/accounts")
    account_id = os.environ.get("OANDA_ACCOUNT_ID") or (resp.json()["accounts"][0]["id"] if resp is not None else "<OANDA_ACCOUNT_ID>")
    print("accounts[0].id: " + str(account_id))
    # GET /v3/accounts/{accountID}/openPositions (version v3): Positions with open trades, netted per instrument with long and short sides.
    resp = request("GET", "/v3/accounts/" + account_id + "/openPositions")
    if resp is not None:
        print("Open positions: " + json.dumps(resp.json())[:600])
    # POST /v3/accounts/{accountID}/orders (version v3): Create an order. Market, limit, stop, market if touched and dependent orders all go through this one endpoint. Returns 201.
    if SEND and any(v in ("...", 0) for v in walk(ORDER)):
        sys.exit("Set every placeholder in ORDER before a real send.")
    resp = request("POST", "/v3/accounts/" + account_id + "/orders", body=ORDER)
    ref = resp.json()["orderCreateTransaction"]["id"] if resp is not None else "<orderCreateTransaction.id from the order reply>"
    print("orderCreateTransaction.id: " + str(ref))
    # GET /v3/accounts/{accountID}/orders/{orderSpecifier} (version v3): The details of one order on the account, whatever its state.
    resp = request("GET", "/v3/accounts/" + account_id + "/orders/" + str(ref))
    if resp is not None:
        print("Confirmation: " + json.dumps(resp.json(), indent=2))
    else:
        print("Dry run finished. Nothing was sent. Run again with the word send as the first argument to send these requests to the demo host.")


def walk(value):
    """Every leaf value in a nested dict or list, for the placeholder check."""
    if isinstance(value, dict):
        for v in value.values():
            yield from walk(v)
    elif isinstance(value, list):
        for v in value:
            yield from walk(v)
    else:
        yield value


if __name__ == "__main__":
    try:
        main()
    except requests.RequestException as err:
        sys.exit("Network error: " + str(err))
```

Status: verified. Source: [developer.oanda.com](https://developer.oanda.com/rest-live-v20/order-ep/), checked 2026-09-25.

## Sources

- Status: verified. Source: [developer.oanda.com](https://developer.oanda.com/rest-live-v20/introduction/), checked 2026-10-12.
- Status: verified. Source: [developer.oanda.com](https://developer.oanda.com/rest-live-v20/development-guide/), checked 2026-10-12.
- Status: verified. Source: [developer.oanda.com](https://developer.oanda.com/rest-live-v20/authentication/), checked 2026-10-12.
- Status: verified. Source: [developer.oanda.com](https://developer.oanda.com/rest-live-v20/account-ep/), checked 2026-09-25.

## OANDA limits to pace against

- Headline limit: 120/s per IP address, HTTP 429 above it
  - Scope: ip
  - Status: verified. Source: [developer.oanda.com](https://developer.oanda.com/rest-live-v20/development-guide/), checked 2026-10-03.
  - Note: Best practices. Also 2 new connections a second. The development guide separately states 120 requests a second per IP address, read 24 Sep 2026; the two figures are in different units and neither replaces the other.

## Next on the OANDA API

- [OANDA API error codes](https://openbrokerapi.com/errors/oanda): 242 decoded, each with what it means, and the fix where the source gives one.
- [OANDA API endpoints](https://openbrokerapi.com/brokers/oanda/endpoints): 33 documented, each with its method, path, parameters and source.
- [OANDA API page](https://openbrokerapi.com/brokers/oanda): The full record: access, authentication, limits and the source for each.
- [Get your OANDA trade history into a spreadsheet](https://openbrokerapi.com/guides/oanda-trade-history): Guide

## Licence and how to cite

The records behind this page are all rights reserved. This file is the page itself, at its own address, in another format. It is not part of the CC BY 4.0 downloads listed at https://openbrokerapi.com/data. Quote from it with a link to the page.

Cite as: OANDA v20 API in Python: token to first call. Broker API Hub, https://openbrokerapi.com/brokers/oanda/quickstart, newest check 2026-09-25.

Where you rely on a single figure, cite the source and the checked date printed beside it as well. Where this page and a broker's own documentation differ, the documentation wins.
