> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://partner.ninjatrader.com/connect/api/rest-api-endpoints/authentication/o-auth-token/llms.txt. > For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://partner.ninjatrader.com/_mcp/server. # O Auth Token POST https://live.tradovateapi.com/v1/auth/oauthtoken Content-Type: application/json ### Exchange a credential for an access token.**Available to:** Registered OAuth client applications and NT Connect partners**Environments:** Demo, Live. The `jwt-bearer` grant is Live in practice, because partner configurations are provisioned there.**[Rate Limit](/overview/core-concepts/rate-limits):** 10 requests per hour, 30-second back-off, counts failed requests only. OAuth errors are returned as `HTTP 200` and are not failures for this purpose, so only transport-level failures count toward the penalty.Select a grant with `grant_type`; which of the other request fields apply depends on the grant you choose.| `grant_type` | Use it to | Required fields | | --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | | `authorization_code` | Redeem a code from the authorize endpoint | `code`, `redirect_uri` | | `refresh_token` | Renew an access token without re-authenticating | `refresh_token` | | `urn:ietf:params:oauth:grant-type:jwt-bearer` | Exchange a signed partner assertion for a token on behalf of one of your users | `assertion` | | `client_credentials` | Authenticate a headless client that acts as its own service account, with no user present. The response carries no refresh token; re-authenticate when the access token expires. | `client_id`, `client_secret` |Authenticate the client with `client_id` and `client_secret` in the body, or with `httpAuth` (base64 `client_id:client_secret`). Public clients use PKCE and send `code_verifier` instead of a secret. For the authorization-code flow, see the [OAuth JavaScript tutorial](https://github.com/tradovate/example-api-oauth).**Warning:** This endpoint returns **HTTP 200** for successes **and** errors. Always check for an `error` field in the response body to determine whether the request succeeded or failed.**Partner SSO: the `jwt-bearer` Grant**NT Connect partners exchange a short-lived JWT, signed with their registered key, for a NinjaTrader access token scoped to one of their users. No user password is involved and no NinjaTrader login prompt is shown.**Assertion Requirements**The assertion must be signed with **RS256** and carry a `kid` header identifying the signing key. Required claims:| Claim | Value | | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `iss` | The issuer registered on your partner configuration. It must match exactly. | | `sub` | The numeric NinjaTrader user ID of the user you are acting for. Users are never auto-provisioned; the account must already exist and belong to your organization. | | `aud` | The audience registered on your partner configuration (in practice the token endpoint URL of the environment you are calling). It must match exactly. | | `iat` | Issued-at time. | | `exp` | Expiry. `exp` minus `iat` must not exceed **300 seconds**; 60 seconds is recommended. | | `jti` | A unique identifier. Reusing one is rejected as a replay. |Clock skew of up to 60 seconds is tolerated on the time claims. Any additional claims you include are ignored.**Signing Keys**NinjaTrader fetches your public keys from the JWKS URL on your partner configuration and caches them for one hour. A key rotation is picked up automatically the first time an assertion arrives with a `kid` that is not in the cache, so publish the new key before you start signing with it.**Sample Call**```bash curl -X POST 'https://live.tradovateapi.com/v1/auth/oauthtoken' \ -H 'Content-Type: application/json' \ -d '{ "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", "assertion": "" }' ``````json { "access_token": "…", "refresh_token": "…", "token_type": "bearer", "expires_in": 86400, "refresh_token_expires_in": 93600 } ```Read the token lifetimes from `expires_in` and `refresh_token_expires_in` rather than hard-coding them. They are environment configuration, and there is no partner-specific override. An `id_token` is only issued on the `authorization_code` grant; it is always null for `jwt-bearer`.**Scopes**The resulting token carries the scopes configured on your partner client application, intersected with the scopes the user has granted. A user who has revoked your application is rejected with `access_denied`.**Refresh Token Behavior**Refresh tokens are single-use and rotate: each refresh consumes the current token and returns a new one. Because a partner session is keyed on the partner and the user rather than on a device, **a new `jwt-bearer` exchange replaces the refresh token issued by the previous exchange for that same user.** Keep the most recent token pair, or re-run the exchange, rather than holding several in parallel.**Signing a User Into a NinjaTrader-Hosted Page**To hand a signed-in user off to a NinjaTrader-hosted page, use the partner access token from this endpoint to mint a short-lived code with [`shortGrantCode`](/api/rest-api-endpoints/authentication/short-grant-code), then redirect the user's browser with that code attached. The access token itself never leaves your backend. The NinjaTrader application redeems the code with [`exchangeShortGrantCode`](/api/rest-api-endpoints/authentication/exchange-short-grant-code).**Common Failure Scenarios**- The assertion is signed with an algorithm other than RS256, or omits the `kid` header. - `iss` or `aud` does not exactly match the values registered on the partner configuration. - The assertion lifetime exceeds 300 seconds. - The same `jti` was already used. - `sub` does not resolve to an active user in your organization. - The partner configuration is disabled or archived. - `assertion` is missing on a `jwt-bearer` call, which matches no grant shape and returns `bad_request`.**Error Messages**| `error` | `error_description` | Trigger | | ------------------------ | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `invalid_client` | `Unknown issuer: ` | No partner configuration matches the assertion's `iss` | | `invalid_client` | `Provider is disabled: ` / `Provider is archived: ` | The partner configuration is not currently usable | | `invalid_client` | `Partner SSO client application is not configured` | The partner's client application is missing or not authorized for this flow | | `invalid_grant` | `Invalid assertion: …` | Expired assertion, bad signature, audience mismatch, a missing `kid`, `sub`, `iat`, `exp`, or `jti`, or a lifetime over the 300-second cap | | `invalid_grant` | `Assertion jti has already been used` | Replayed assertion | | `invalid_grant` | `User not found: userId=` | `sub` is a valid user ID but no such user exists. This is the usual shape. | | `invalid_grant` | `User not found: sub=` | `sub` is not a positive integer, so it never resolved to a user ID | | `invalid_grant` | `User account is not active: ` | The user account is inactive | | `invalid_grant` | The user is not in your partner organization | The user belongs to a different organization. The message also reports the organization IDs involved; treat that detail as diagnostic rather than a stable format. | | `access_denied` | `User has denied this application` | The user revoked your application | | `unsupported_grant_type` | `Supported: authorization_code, refresh_token, client_credentials, urn:ietf:params:oauth:grant-type:jwt-bearer` | An unrecognized `grant_type` from a public client. Confidential clients get `bad_request` instead. | | `bad_request` | `Invalid request` | The request shape matched no supported grant, including an unrecognized `grant_type` sent with a client secret | | `server_error` | `Internal error: …` | Unexpected server-side failure. The detail relays the underlying error text; do not parse it. | Reference: https://partner.ninjatrader.com/connect/api/rest-api-endpoints/authentication/o-auth-token ## Request ### Body (application/json) This endpoint expects an OAuthToken. - `grant_type` (string, required) - `code` (string, optional) - `redirect_uri` (string, optional) - `client_id` (string, optional) - `client_secret` (string, optional) - `httpAuth` (string, optional) - `refresh_token` (string, optional) - `code_verifier` (string, optional) - `resource` (string, optional) - `assertion` (string, optional) ## Response ### 200 OAuthTokenResponse - `access_token` (string, optional) - `refresh_token` (string, optional) - `token_type` (string, optional) - `expires_in` (integer, optional) - `refresh_token_expires_in` (integer, optional) - `error` (string, optional) - `error_description` (string, optional) - `id_token` (string, optional) ## Examples **Request** ```json { "grant_type": "string" } ``` **Response** ```json { "access_token": "string", "refresh_token": "string", "token_type": "string", "expires_in": 1, "refresh_token_expires_in": 1, "error": "string", "error_description": "string", "id_token": "string" } ``` **SDK Code** ```python import requests url = "https://live.tradovateapi.com/v1/auth/oauthtoken" payload = { "grant_type": "string" } headers = {"Content-Type": "application/json"} response = requests.post(url, json=payload, headers=headers) print(response.json()) ``` ```javascript const url = 'https://live.tradovateapi.com/v1/auth/oauthtoken'; const options = { method: 'POST', headers: {'Content-Type': 'application/json'}, body: '{"grant_type":"string"}' }; try { const response = await fetch(url, options); const data = await response.json(); console.log(data); } catch (error) { console.error(error); } ``` ```go package main import ( "fmt" "strings" "net/http" "io" ) func main() { url := "https://live.tradovateapi.com/v1/auth/oauthtoken" payload := strings.NewReader("{\n \"grant_type\": \"string\"\n}") req, _ := http.NewRequest("POST", url, payload) req.Header.Add("Content-Type", "application/json") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(res) fmt.Println(string(body)) } ``` ```ruby require 'uri' require 'net/http' url = URI("https://live.tradovateapi.com/v1/auth/oauthtoken") http = Net::HTTP.new(url.host, url.port) http.use_ssl = true request = Net::HTTP::Post.new(url) request["Content-Type"] = 'application/json' request.body = "{\n \"grant_type\": \"string\"\n}" response = http.request(request) puts response.read_body ``` ```java import com.mashape.unirest.http.HttpResponse; import com.mashape.unirest.http.Unirest; HttpResponse response = Unirest.post("https://live.tradovateapi.com/v1/auth/oauthtoken") .header("Content-Type", "application/json") .body("{\n \"grant_type\": \"string\"\n}") .asString(); ``` ```php request('POST', 'https://live.tradovateapi.com/v1/auth/oauthtoken', [ 'body' => '{ "grant_type": "string" }', 'headers' => [ 'Content-Type' => 'application/json', ], ]); echo $response->getBody(); ``` ```csharp using RestSharp; var client = new RestClient("https://live.tradovateapi.com/v1/auth/oauthtoken"); var request = new RestRequest(Method.POST); request.AddHeader("Content-Type", "application/json"); request.AddParameter("application/json", "{\n \"grant_type\": \"string\"\n}", ParameterType.RequestBody); IRestResponse response = client.Execute(request); ``` ```swift import Foundation let headers = ["Content-Type": "application/json"] let parameters = ["grant_type": "string"] as [String : Any] let postData = JSONSerialization.data(withJSONObject: parameters, options: []) let request = NSMutableURLRequest(url: NSURL(string: "https://live.tradovateapi.com/v1/auth/oauthtoken")! as URL, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 10.0) request.httpMethod = "POST" request.allHTTPHeaderFields = headers request.httpBody = postData as Data let session = URLSession.shared let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in if (error != nil) { print(error as Any) } else { let httpResponse = response as? HTTPURLResponse print(httpResponse) } }) dataTask.resume() ```