Tradovate API

Tradovate Access Token 400 'Invalid JSON' Error

You send correct credentials to the access-token endpoint and get a 400 “invalid JSON” anyway. Here's every formatting mistake that causes it, and the corrected request for curl, Python, and JavaScript.

Reviewed by the PickMyTrade Trading Systems Team Last updated
· 7 min read
REST client showing a 400 response with an invalid JSON error message on the access token request

You send your username and password to the access-token endpoint, and instead of a token you get a blunt 400 with a message like [Invalid JSON: expected '}' or ',', offset: 0x00000028]. The credentials are fine. The account works in the web platform. So what gives?

Here's the short answer: the server never read your login at all. A 400 “invalid JSON” happens at the parsing stage, before any credential check. Something about the shape of your request body, the quotes, the header, the way your language serialized it, isn't valid JSON. Fix the formatting and the exact same credentials sail through.

This guide walks through every version of that formatting mistake, in the order you're most likely to hit it, with the corrected request for curl, Python, and JavaScript.

What the Error Actually Looks Like

The request goes to the authentication endpoint. On the demo environment that's:

POST https://demo.tradovateapi.com/v1/auth/accesstokenrequest

On live it's https://live.tradovateapi.com/v1/auth/accesstokenrequest. Same body, different host, mixing them up is its own problem, but it won't produce an “invalid JSON” message, so set that aside for now.

A successful call returns a JSON object with an accessToken and an expirationTime. A malformed one returns HTTP 400 and a body that names the parser's complaint and a byte offset. That offset is your best clue, and we'll come back to it.

The Real Cause: Your Body Isn't Valid JSON

JSON has strict rules that a lot of code quietly breaks. The most common ways a login body ends up invalid:

  • Single quotes. JSON demands double quotes around keys and string values. If you print a Python dict, a JavaScript object, or a Ruby hash straight into the request, you get single quotes and it's no longer JSON.
  • A trailing comma. A comma after the last field is legal in most languages and illegal in JSON.
  • Form encoding. Many HTTP libraries default to application/x-www-form-urlencoded. The server expecting JSON chokes on the very first byte.
  • Double encoding. You stringify the object once, then hand that string to a client that stringifies it again. Now the body is a quoted string, not an object.
  • Hidden characters. A byte-order mark or a “smart quote” pasted from a doc or chat looks identical on screen but breaks the parser.

Every one of these produces the same family of 400 errors. Let's kill them one at a time.

Fix 1: Send Real Double-Quoted JSON

Start with the target. This is what a valid body looks like, double quotes everywhere, no trailing comma, cid as a bare number:

{ "name": "your_username", "password": "your_password", "appId": "My App", "appVersion": "1.0", "cid": 8, "sec": "your-api-secret", "deviceId": "123e4567-e89b-12d3-a456-426614174000" }

Only name and password are strictly required; the rest identify your application and are needed for full API access. Compare that against the broken version people usually send, which is a language object printed as text:

{'name': 'your_username', 'password': 'your_password', 'appId': 'My App', 'cid': '8'}

Two problems there: single quotes throughout, and cid wrapped in quotes so it reads as a string. Swap the single quotes for double quotes and unquote the cid value, and the body validates.

Side by side comparison of a broken single-quoted request body and a corrected double-quoted JSON body

Fix 2: Set the Content-Type Header

Valid JSON in the body isn't enough. You also have to tell the server the body is JSON. Add these headers to the request:

Content-Type: application/json
Accept: application/json

Without Content-Type: application/json, most clients fall back to form encoding and the server tries to parse form fields as a JSON document. The parse fails at byte zero and you get a 400. This one header fixes a surprising share of “invalid JSON” reports on its own.

Fix 3: In Python, Use json= Not data=

The requests library trips people here constantly. If you pass your dictionary through the data parameter, it gets form-encoded and the double quotes never appear. Pass it through json instead, that serializes the dict to proper JSON and sets the Content-Type header automatically.

Wrong: requests.post(url, data=credentials)
Right: requests.post(url, json=credentials)

With json=credentials you never touch quoting by hand, the library does it correctly. Just make sure credentials is a real dict (with cid as an int), not a pre-stringified blob. If you already called json.dumps() on it, either drop that and use json=, or keep the string and pass it through data= with the Content-Type header set manually. Don't do both.

HTTP client headers tab showing Content-Type application/json with a raw JSON body ready to send

Fix 4: In JavaScript, Stringify Exactly Once

With fetch, the classic mistake is forgetting JSON.stringify (so the body becomes the useless string [object Object]) or calling it twice. Do it once, and set the header:

fetch(url, {
  method: "POST",
  headers: { "Content-Type": "application/json", "Accept": "application/json" },
  body: JSON.stringify(credentials)
})

If credentials is already a string, don't wrap it in JSON.stringify again, the second pass escapes every quote and the server receives one long quoted string, which is not an object.

Fix 5: With curl, Escape Your Quotes

The shell eats quote characters, so a raw JSON payload on the command line needs care. Escape the inner double quotes with backslashes:

curl -X POST https://demo.tradovateapi.com/v1/auth/accesstokenrequest \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d "{ \"name\": \"your_username\", \"password\": \"your_password\", \"appId\": \"My App\", \"appVersion\": \"1.0\", \"cid\": 8, \"deviceId\": \"123e4567-e89b-12d3-a456-426614174000\", \"sec\": \"your-api-secret\" }"

An easier route: put the JSON in a file and pass -d @body.json, which sidesteps shell quoting entirely. Note that plain -d defaults to form encoding, so the Content-Type: application/json header still has to be explicit.

Fix 6: Check Your Field Types

Even clean JSON gets rejected if a value has the wrong type. The one that bites people is cid: it's an integer, so send "cid": 8, never "cid": "8". Keep the string fields (name, password, appId, appVersion, sec, deviceId) quoted, and leave cid as a bare number. Here's the quick reference:

Field Type Required? Example
namestringYes"trader_jane"
passwordstringYes"S3cureP@ss!"
appIdstringFor full access"My App"
appVersionstringFor full access"1.0"
cidintegerFor full access8
secstringFor full access"f03741b6-..."
deviceIdstringRecommended"123e4567-..."

If your password contains a double quote, a backslash, or a newline, those characters have to be escaped inside the JSON string. Letting your serializer build the body (the json= and JSON.stringify approaches above) handles that for you.

Reading the Offset in the Error

When the message hands you an offset like 0x00000028, use it. The value is hexadecimal, 0x28 is decimal 40, and it points at the byte where the parser stopped. Count into the exact body you sent (log it, don't guess) and inspect that character. A single quote, a comma with nothing after it, or a control character is almost always sitting right there. An offset of 0x0 is different: it means the parser found nothing to read, so your body was empty or never left the client.

Verify the Fix

Once the body is clean and the header is set, re-run the request. A working call returns HTTP 200 with a JSON payload containing your accessToken, a userId, and an expirationTime about 90 minutes out. Copy that token into an Authorization: Bearer <token> header for every follow-up call.

Successful 200 response returning an access token and expiration time from the access token request

If you keep getting the same 400, log the raw bytes of what you're actually sending, not what you think you're sending, and diff it against the valid example above. The difference is nearly always one quote or one comma.

When the JSON Is Clean and It Still Fails

If the parser is happy but the request still errors, you've moved past the formatting problem and into an authentication or environment problem:

  • Wrong host. Demo credentials on the live host (or vice versa) fail even with perfect JSON.
  • Bad API secret or cid. An incorrect sec or cid returns a 400 that has nothing to do with formatting, the body parsed fine, the values just don't match.
  • Session limit or rate limit. Hammering the endpoint can lock you out. If you see repeated failures under load, read our note on the Tradovate API 429 rate limit.
  • Expired token on later calls. The token itself only lasts about 90 minutes; renew it before it dies rather than re-authenticating each time. See Tradovate API 401 unauthorized (expired token).

Simplify Auth With PickMyTrade

Rather not babysit token requests, 90-minute expiries, and JSON quoting at all? PickMyTrade connects your Tradovate account and routes your TradingView alerts to live orders, the authentication and session handling run behind the scenes, so there's no auth code for you to debug.

Skip the Auth Code Entirely

PickMyTrade connects your Tradovate account and handles token requests, renewals, and JSON formatting behind the scenes, so your alerts route without you debugging a single auth call.

Start Your Free 5-Day Trial

Frequently Asked Questions

The 400 is thrown before the server checks your login. The body isn't valid JSON, or it wasn't sent as JSON. Look for single quotes, a trailing comma, form encoding, or a missing Content-Type: application/json header. Fix the body and the same credentials authenticate fine.

It's the byte position where the JSON parser stopped, written in hex. 0x28 is decimal 40, so look around the 40th character of the exact body you sent, that's where the syntax broke. An offset of 0x0 means the body was empty or never arrived.

A number. Send "cid": 8, not "cid": "8". Quoting it turns an integer field into a string and can get the request rejected even when the JSON is otherwise valid.

The explorer builds a clean double-quoted body and sets the header for you. Your client may be printing a language object with single quotes, form-encoding the data, or double-encoding the string. Copy the explorer's exact body and match it byte for byte.

Yes. Escape the inner double quotes with backslashes, or wrap the whole payload in single quotes so the shell doesn't touch the inner double quotes. Passing the body from a file with -d @body.json avoids the quoting dance altogether.

This guide is for educational and informational purposes only and is not financial, investment, or trading advice. Trading futures and other leveraged products carries a substantial risk of loss and is not suitable for every investor. PickMyTrade is an independent third-party automation platform and is not affiliated with, endorsed by, or sponsored by Tradovate, Inc. or Bookmap. All related names, logos, and trademarks are the property of their respective owners. Platform features and steps change over time, so always confirm the current process in the official platform documentation before acting.