# OANDA API Python: v20 tokens, practice account and limits

URL: https://fincodelabs.com/en/blog/oanda-api-python · Publisher: Fincode (Fincode Yazılım ve Teknoloji A.Ş.), Istanbul, Turkey · Category: Stocks, forex and broker APIs · Published: 2026-11-06 · Updated: 2026-11-06

OANDA API Python guide: get a v20 token, practice and live hosts, documented limits and conflicts, v20 binding vs requests, and order handling tested on a mock.

**Short answer:** To use the **OANDA API from Python**, you need a v20 account, a personal access token created under Manage API Access, and HTTPS calls with that token to `api-fxpractice.oanda.com` for practice or `api-fxtrade.oanda.com` for live. OANDA's documentation, read on 5 November 2026, says v20 is available to all divisions except OANDA Global Markets and OANDA TMS BROKERS S.A., and allows 120 requests per second per IP. The official `v20` package was last released on 28 September 2018 and failed on import here until PyYAML was added, so plain `requests` is a fair alternative.

Leveraged FX and CFDs can lose money quickly: the banner on OANDA's developer pages, as shown on 5 November 2026, says 76.6% of retail investor accounts lose money when trading CFDs with this provider, and their footer says 76.8%. Treat everything here as software documentation, not a reason to trade. No broker is recommended, ranked or linked with a referral; rules depend on your country, and account types and API access vary.

Official pages were read on 5 November 2026. No token or practice account was available, so all code ran against a local mock built from the documented responses, and the footer dates on the developer pages run from 1996 to 2019, so recheck anything that matters.

Section 3 of TradingView's [Terms of Use](https://www.tradingview.com/policies/), read on 5 November 2026, licenses charts, alerts and webhooks for display-only use, lists automated trading and automated order generation among prohibited uses, and bars third-party products built for that purpose. That applies to any TradingView bridge to OANDA; it is a contract question between you and TradingView, so read the terms and take legal advice.

## OANDA API key: how to get a v20 token

OANDA calls the key a personal access token, and you generate it from your account after accepting the API license.

1. Open a v20 account, live or practice.
2. Log in and find the token page. The [developer introduction](https://developer.oanda.com/rest-live-v20/introduction/) says My Services, then Manage API Access; a [help article](https://help.oanda.com/us/en/faqs/rest-v20-api-troubleshooting-guide.htm) says the Hub, under Tools, then API. Layouts differ, so look for both.
3. Agree to the license and generate the token.
4. Store it at once: the [authentication page](https://developer.oanda.com/rest-live-v20/authentication/) says OANDA does not retain it, and a lost token must be revoked and replaced. See [API key security](https://fincodelabs.com/en/blog/api-key-security-trading-bots).
5. Send it as `Authorization: Bearer` and call `GET /v3/accounts` to read your account IDs.

**Token and account errors, official pages read 5 November 2026**

| Message | What the pages say |
| --- | --- |
| `Insufficient authorization to perform request.` | Listed under 401 (bad token or wrong URL) and under 403 (account cannot trade), so check the token and host first |
| `The provided request was forbidden.` | Listed under 401 on the developer page; the help article names no status and says it usually means a wrong account ID |

The authentication page says one token reaches all sub-accounts, while the help article says new sub-accounts need a new token. If a new sub-account returns an error, try that first.

## OANDA API pricing: who can use v20 and what it costs

Availability depends on your OANDA division, and no page read lists a fee for API access itself.

The introduction names two divisions that are excluded: OANDA Global Markets and OANDA TMS BROKERS S.A. The [Singapore REST API page](https://www.oanda.com/sg-en/platforms/rest-api/) advertises a free demo environment and requires an application, approval, funding and acceptance of an API agreement, with no stated minimum. Check the page for your own division. Accounts linked to MT4 have a further rule: the order definitions say not to set client extensions on them.

**Exchange rates are a separate product.** `developer.oanda.com/exchange-rates-api/` redirected on 5 November 2026 to `api.corporatefxservices.com`, whose OpenAPI file describes a separate API key from its own portal. OANDA's [API plans page](https://www.oanda.com/foreign-exchange-data-services/en/exchange-rates-api/api-plans/), read the same day, lists Lite at $450 a month or $4,850 a year up to Premium + Crypto at $1,680 a month or $17,000 a year. A v20 token belongs to your trading account, and nothing read says it covers commercial display of rates, so check the API agreement.

## OANDA API limits, and where the documents disagree

The Development Guide gives firm numbers, the Best Practices page gives a lower recommendation, and neither explains what happens after a 429. The OANDA API documentation lives at `developer.oanda.com`.

**v20 limits, developer pages read 5 November 2026**

| Item | Value | Source |
| --- | --- | --- |
| REST requests | 120 a second per IP, then HTTP 429 | [Development Guide](https://developer.oanda.com/rest-live-v20/development-guide/) |
| REST on one connection | 100 a second recommended | [Best Practices](https://developer.oanda.com/rest-live-v20/best-practices/) |
| New connections | 2 a second | Both pages |
| Active streams | 20 per IP; more are rejected | Development Guide |
| Stream prices | At most 4 a second per instrument; heartbeat every 5 seconds | [Pricing](https://developer.oanda.com/rest-live-v20/pricing-ep/) |

Hosts: REST on `api-fxpractice.oanda.com` and `api-fxtrade.oanda.com`, streams on `stream-fxpractice.oanda.com` and `stream-fxtrade.oanda.com`. Design to 100 a second on a kept-alive connection. Other gaps:

- The pages read mention no `Retry-After` header, so choose your own back-off.
- The Singapore page mentions WebSockets; the developer pages show chunked HTTP on the stream hosts, and that is what the sample below reads.
- Prices skipped within a 250-millisecond window never arrive, so the stream is not a full tick record.

## OANDA v20 Python binding or plain requests?

Both reached the same mock responses, so the choice is about upkeep rather than features.

**Run on 5 November 2026 with Python 3.11.3 and pip 22.3.1 and 26.3, against the mock**

| Check | `v20` 3.0.25.0 | `requests` |
| --- | --- | --- |
| Last release | 28 September 2018; the newest egg on GitHub targets Python 3.7, and PyPI classifiers name 2.7 and 3.5 | 2.34.2 as installed |
| Fresh install | `import v20` failed with `No module named 'yaml'`; worked after `pip install pyyaml` | Worked |
| Order and stream calls | Worked; `r.get(field, 201)` raises `ResponseUnexpectedStatus` on any other status | Worked; you read fields yourself |
| License signal | MIT on GitHub and in PyPI's license field; PyPI's classifier says Other/Proprietary | Apache 2.0 |

The binding gives typed responses but tracks no API change after 2018; plain `requests` means you own the field names. The community package [oandapyV20](https://pypi.org/project/oandapyV20/), last released as 0.7.2 on 27 August 2021, was not run; OANDA's help article calls it third-party and unsupported.

## OANDA v20 Python example: orders and prices

This client treats HTTP 201 as a created order, not a fill, and never resends after a timeout or dropped connection without looking first. Create it with `V20(os.environ["OANDA_TOKEN"], account_id)`.

```python
import json, requests

class V20:
    """Plain requests client; keep the token out of the code."""
    def __init__(self, token, account, rest="https://api-fxpractice.oanda.com",
                 stream="https://stream-fxpractice.oanda.com"):
        self.account, self.rest, self.stream_host = account, rest, stream
        self.http = requests.Session()           # keep-alive, as Best Practices asks
        self.http.headers.update({"Authorization": f"Bearer {token}",
                                  "Accept-Datetime-Format": "RFC3339"})

    def market(self, instrument, units, client_id, timeout=10):
        url = f"{self.rest}/v3/accounts/{self.account}/orders"
        order = {"type": "MARKET", "instrument": instrument, "units": str(units),
                 "timeInForce": "FOK", "positionFill": "DEFAULT",
                 "clientExtensions": {"id": client_id}}
        try:
            r = self.http.post(url, json={"order": order}, timeout=timeout)
        except (requests.Timeout, requests.ConnectionError):
            return self.find(client_id)          # never resend blind
        body = r.json() if "json" in r.headers.get("Content-Type", "") else {}
        if r.status_code == 201:                 # 201 does not mean filled
            if "orderFillTransaction" in body:  # its price field is deprecated
                return "FILLED", body["orderFillTransaction"].get("fullVWAP")
            if "orderCancelTransaction" in body:
                return "CANCELLED", body["orderCancelTransaction"]["reason"]
            return "OPEN", body["orderCreateTransaction"]["id"]
        if r.status_code == 429:
            return "RATE_LIMITED", None          # back off, do not loop
        reject = body.get("orderRejectTransaction", {}).get("rejectReason")
        return f"HTTP_{r.status_code}", reject or body.get("errorMessage")

    def find(self, client_id):
        url = f"{self.rest}/v3/accounts/{self.account}/orders/@{client_id}"
        r = self.http.get(url, timeout=10)
        if r.status_code == 404:
            return "NOT_FOUND", None
        r.raise_for_status()                     # a 429 or 5xx says nothing about the order
        return f"FOUND_{r.json()['order']['state']}", None

    def prices(self, instruments, read_timeout=15):
        """Yield PRICE dicts; read_timeout is three of the documented 5 s heartbeats."""
        url = f"{self.stream_host}/v3/accounts/{self.account}/pricing/stream"
        with self.http.get(url, params={"instruments": instruments}, stream=True,
                           timeout=(5, read_timeout)) as r:
            r.raise_for_status()
            for line in filter(None, r.iter_lines()):
                msg = json.loads(line)
                if msg["type"] == "PRICE":
                    yield msg
```

Output against the mock, including the failure paths. The account ID, token and prices are synthetic; **not run** on a practice or live account, so try it there first as described in [paper trading API](https://fincodelabs.com/en/blog/paper-trading-api):

```
fill      ('FILLED', '1.15012')
cancelled ('CANCELLED', 'MARKET_HALTED')
reject    ('HTTP_400', 'UNITS_PRECISION_EXCEEDED')
429       ('RATE_LIMITED', None)
timeout   ('FOUND_FILLED', None)
dropped   ('FOUND_FILLED', None)
lookup 503 raises HTTPError
price     1.15000
bad token ('HTTP_401', 'Insufficient authorization to perform request.')
bad acct  ('HTTP_401', 'The provided request was forbidden.')
stream stalled: ConnectionError
```

What each case shows:

- **201 is not a fill.** The [order endpoint](https://developer.oanda.com/rest-live-v20/order-ep/) returns 201 with `orderCancelTransaction` when a fill-or-kill order is canceled, for example with reason `MARKET_HALTED`. The [transaction definitions](https://developer.oanda.com/rest-live-v20/transaction-df/) mark the fill's `price` deprecated, so the code reads `fullVWAP`.
- **Units are signed strings.** Positive is long, negative is short.
- **Timeouts and dropped connections.** The order specifier accepts `@clientID`, so the code looks the order up. Whether a filled market order stays retrievable that way could only be run on the mock, so confirm it on practice. Any lookup status except 404 raises, and a 404 right after a timeout does not prove the order never arrived, so look again before resending.
- **Duplicates.** `CLIENT_ORDER_ID_ALREADY_EXISTS` is defined against another pending order, and a filled order is no longer pending, so keep your own duplicate check; see [client order ID](https://fincodelabs.com/en/blog/client-order-id).
- **Stalled stream.** With no heartbeat inside the read timeout, `requests` raised `ConnectionError` here, so wrap `prices()` in a reconnect loop with back-off.
- **Position fill.** `DEFAULT` reduces first on non-hedging accounts and opens a new trade on hedging accounts, so one code path can behave differently per account.

> **Hypothetical example.** An alert sends 10,000 units. The POST times out at 10 seconds, and the worker resends. If the first request had filled, the account now holds 20,000 units at twice the intended risk. With the lookup, the worker finds the order and stops. Limits on size belong outside this code, as in [trading bot risk management](https://fincodelabs.com/en/blog/trading-bot-risk-management).

## How to connect TradingView to OANDA

TradingView lists OANDA as a broker for trading from the chart, but that link is not automation; alerts need your own bridge calling v20.

TradingView's [broker directory](https://www.tradingview.com/broker/OANDA/) has an OANDA profile, and its [Help Center](https://www.tradingview.com/support/solutions/43000481026-how-to-autotrade-using-pine-script-strategies/) says automated strategy trading with a brokerage account is not available yet. A bridge is alert, your HTTPS receiver, then the order code above; see [TradingView webhook bot](https://fincodelabs.com/en/blog/tradingview-webhook-bot). Use alerts from rules you wrote, on your own account, never other people's. Order design beyond this page is in [broker API integration](https://fincodelabs.com/en/blog/broker-api-integration-fix-rest).

## Leveraged FX rules by country, including Turkey

Whether you may use a given broker depends on where you live, so check your own regulator before you open an account or write a line of code.

**Regulator pages read 5 November 2026; later changes not checked**

| Where | What the regulator says |
| --- | --- |
| Turkey | SPK [said on 23 February 2023](https://spk.gov.tr/duyurular/basin-duyurulari/2023/izinsiz-kaldiracli-islemlere-iliskin-basin-duyurusu) that residents may carry out leveraged FX trades only through firms it has authorized; offering them without authorization is a crime under Article 109(2) of [Law No. 6362](https://www.mevzuat.gov.tr/mevzuatmetin/1.5.6362.pdf), with two to five years in prison and a judicial fine. Article 99(4) lets SPK block such sites, and [Communiqué III-37.1](https://mevzuat.spk.gov.tr/api/Mevzuat/File/130) Articles 9 and 27 cap leverage at authorized firms at 10:1 and treat a Turkish website or marketing as activity aimed at residents. Take legal advice before using a foreign firm |
| United States | The [CFTC advisory](https://www.cftc.gov/LearnAndProtect/AdvisoriesAndArticles/CustomerAdvisory_MustKnowForex.html) lists leverage above the US legal limit (2 percent on major pairs, 5 percent on others) as a warning sign, warns about unregistered offshore dealers and points to NFA BASIC for checks |
| United Kingdom | The [FCA's rules](https://www.fca.org.uk/news/press-releases/fca-confirms-permanent-restrictions-sale-cfds-and-cfd-options-retail-consumers), announced 1 July 2019 and applying from 1 August 2019, cap retail CFD leverage, including rolling spot forex, at 30:1 to 2:1 |
| European Union | [ESMA's temporary 2018 measures](https://www.esma.europa.eu/press-news/esma-news/esma-agrees-prohibit-binary-options-and-restrict-cfds-protect-retail-investors) set 30:1 on major pairs, and national regulators found 74% to 89% of retail accounts lose money on CFDs |

Fincode quotes integration work through its [exchange API integration](https://fincodelabs.com/en/services/exchange-api-integration) page, for foreign customers under their own law, so it has an interest here. A Turkish resident asking for a foreign leveraged FX account is declined or sent to a lawyer first. No prior OANDA project is claimed.

## Frequently asked questions

### Is the OANDA API free?

No page read on 5 November 2026 lists a fee for API access, and the Singapore page advertises a free demo environment. Trading costs on the account are separate, terms can differ by division, and the exchange rates API is a separate paid product.

### Where do I find my OANDA API token?

Under Manage API Access in your account, or under Tools then API in the Hub, depending on the page you follow. OANDA does not retain it, so a lost token must be revoked and replaced.

### Why do I get a 401 or 403 from the v20 API?

Check first that the host matches the account's environment, that the Bearer header is set and that the account ID came from GET /v3/accounts. OANDA's developer page lists one message under both codes, so do not branch on the text alone.

### Can I test without real money?

Yes, a practice account uses api-fxpractice.oanda.com. A practice run shows how the code behaves, not how it would fill or perform live.

## Sources

- [OANDA Developer: Introduction (v20 requirements, divisions, Manage API Access)](https://developer.oanda.com/rest-live-v20/introduction/)
- [OANDA Developer: Authentication](https://developer.oanda.com/rest-live-v20/authentication/)
- [OANDA Developer: Development Guide (hosts, rate limits)](https://developer.oanda.com/rest-live-v20/development-guide/)
- [TradingView: Terms of Use, section 3 (read 5 November 2026)](https://www.tradingview.com/policies/)
- [OANDA Developer: Troubleshooting and Errors](https://developer.oanda.com/rest-live-v20/troubleshooting-errors/)
- [OANDA Developer: Order endpoints](https://developer.oanda.com/rest-live-v20/order-ep/)
- [OANDA Developer: Order definitions (MarketOrderRequest, OrderSpecifier, position fill)](https://developer.oanda.com/rest-live-v20/order-df/)
- [OANDA Developer: Transaction definitions (reject and cancel reasons)](https://developer.oanda.com/rest-live-v20/transaction-df/)
- [SPK (Capital Markets Board): Press notice on unauthorized leveraged transactions, 23 February 2023 (Turkish)](https://spk.gov.tr/duyurular/basin-duyurulari/2023/izinsiz-kaldiracli-islemlere-iliskin-basin-duyurusu)
- [OANDA Developer: Sample Code and libraries](https://developer.oanda.com/rest-live-v20/sample-code/)
- [OANDA Developer: Release Notes (3.0.25, 28 September 2018)](https://developer.oanda.com/rest-live-v20/release-notes/)
- [OANDA Help: REST v20 API troubleshooting guide](https://help.oanda.com/us/en/faqs/rest-v20-api-troubleshooting-guide.htm)
- [OANDA: v20-python repository](https://github.com/oanda/v20-python)
- [PyPI: v20 3.0.25.0](https://pypi.org/project/v20/)
- [OANDA Exchange Rates API documentation (own API key, read through its openapi.json)](https://api.corporatefxservices.com/)

> This guide is general information about software and markets, not investment, legal or tax advice. Automated trading can lose money, past or backtested performance does not guarantee future results, and the rules that apply depend on your jurisdiction and your exchange's terms. Sources, checks and corrections: see the [editorial policy](https://fincodelabs.com/en/editorial-policy).

---

Published by Fincode (Fincode Yazılım ve Teknoloji A.Ş.), a software company in Istanbul, Turkey: https://fincodelabs.com/en/blog/oanda-api-python

Cite as: Fincode, "OANDA API Python: v20 tokens, practice account and limits", updated November 6, 2026, https://fincodelabs.com/en/blog/oanda-api-python
