Partner Token

View as Markdown

Partner SSO lets a user who is already signed in to your platform reach NinjaTrader without a second login. Your backend signs a short-lived JWT identifying the user, exchanges it for a NinjaTrader access token, and the user arrives signed in. No NinjaTrader password is involved.

You provide identity. NinjaTrader provides everything else: authorization, account access, trading permissions, and billing.

How the Flow Works

  1. The user signs in to your platform, which you authenticate.
  2. The user chooses to open NinjaTrader from your application.
  3. Your backend issues a short-lived JWT identifying that user.
  4. Your backend posts the JWT to oAuthToken using the jwt-bearer grant.
  5. NinjaTrader verifies the assertion, resolves the user, and returns access and refresh tokens.

The assertion travels server to server, so it never appears in a URL, browser history, deep link, or any other user-visible channel. Only the resulting access token reaches the user’s device.

You operate two things: a JWT issuer that signs short-lived assertions, and a JWKS endpoint that publishes your current public keys. NinjaTrader operates verification and token issuance.

Prerequisites

Before writing code, send NinjaTrader the following so your platform can be registered. Provide one set per environment.

FieldDescription
Platform nameThe name shown in any NinjaTrader interface that mentions your platform
issThe exact value your assertions will carry in the iss claim
JWKS URLAn HTTPS URL where NinjaTrader can fetch your JWKS
audThe exact value your assertions will carry in the aud claim, which is the token endpoint URL for that environment

You also need to agree on the NinjaTrader user IDs for your users, and on the test users you will use before go-live. NinjaTrader confirms the exact audience URL per environment and the schedule for enabling your integration.

Signing the Assertion

Sign with RS256, the only algorithm currently supported. We recommend a 2048-bit key or larger; NinjaTrader does not enforce a minimum key size. The header must carry a kid that matches a key published at your JWKS URL.

1{
2 "alg": "RS256",
3 "kid": "key-2026-01"
4}

Required claims:

1{
2 "iss": "https://auth.samplepartner.com",
3 "sub": "123456",
4 "aud": "https://live.tradovateapi.com/v1/auth/oauthtoken",
5 "iat": 1731843200,
6 "exp": 1731843260,
7 "jti": "3f2504e0-4f89-11d3-9a0c-0305e82c3301"
8}

Keep exp minus iat short. 60 seconds is recommended and 300 seconds is the hard cap, above which the assertion is rejected. NinjaTrader tolerates 60 seconds of clock skew on the time claims. Reusing a jti is rejected as a replay, so generate a fresh UUID for every assertion.

Any additional claims you include are ignored. Authorization is always determined from the resolved NinjaTrader user, never from your assertion.

For the full claim-by-claim requirements, see oAuthToken.

Mapping Users with the sub Claim

The sub claim identifies which NinjaTrader user is signing in. It must be the numeric NinjaTrader user ID of an account that already exists.

There is no partner-facing lookup API, so the mapping is established during onboarding by one of three routes: NinjaTrader provisions the accounts and returns the IDs, you provision through the Admin Dashboard and read the IDs there, or the two sides agree a one-time export to seed the mapping. Whichever route you use, you must already know the NinjaTrader user ID when you issue the assertion.

Store that ID alongside your own user record and read it at sign-in time.

Warning: Users are never auto-provisioned. If a user has no NinjaTrader account, you cannot sign them in this way. The account must be created through normal onboarding or arranged in bulk with NinjaTrader first.

Publishing Your Keys

NinjaTrader fetches your public keys to verify signatures, so your JWKS endpoint must be:

  • Served over HTTPS. Plain HTTP is not fetched.
  • Publicly reachable with no authentication, since it publishes public keys only.
  • In standard JWKS JSON format, per RFC 7517.
1{
2 "keys": [
3 {
4 "kty": "RSA",
5 "use": "sig",
6 "alg": "RS256",
7 "kid": "key-2026-01",
8 "n": "0vx7agoebGcQSuuPiLJXZptN9nndr…",
9 "e": "AQAB"
10 }
11 ]
12}

Responses are cached for one hour. When an assertion arrives with a kid that is not in the cache, NinjaTrader refetches once before failing, which is what makes rotation work without coordination.

Rotating Keys

  1. Generate a new RSA key pair and give it a new kid.
  2. Publish the new public key alongside the old one.
  3. Switch your signer to the new key and its kid.
  4. Remove the old public key once every assertion signed with it has expired.

Rotate at least once a year, and keep both keys published for at least twice your maximum assertion lifetime so in-flight assertions still verify.

Exchanging the Assertion for a Token

$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\":\"$ASSERTION\"}"
NinjaTrader accepts a JSON body on this endpoint rather than the OAuth-default application/x-www-form-urlencoded. This is a platform-wide convention across all NinjaTrader auth endpoints, so send Content-Type: application/json.

A successful exchange returns:

1{
2 "access_token": "",
3 "refresh_token": "",
4 "token_type": "bearer",
5 "expires_in": 86400,
6 "refresh_token_expires_in": 93600
7}

Read the 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 and is always null for jwt-bearer.

Refreshing a Session

Do not exchange a new assertion to refresh. Use the standard refresh-token grant on the same endpoint:

1{
2 "grant_type": "refresh_token",
3 "refresh_token": "<previously-issued>"
4}

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 pair rather than holding several in parallel.

Handling Errors

Warning: This endpoint returns HTTP 200 for successes and errors. Do not branch on the status code. Check whether access_token is present, or whether an error field is present, to determine the outcome.
1{
2 "error": "invalid_grant",
3 "error_description": "User not found: userId=1234"
4}
errorWhat it meansWhat to do
invalid_clientYour iss is not registered, or your configuration is disabled or archivedContact NinjaTrader. Your registration may need updating.
invalid_grantThe assertion failed validation: expired, bad signature, wrong audience, a missing claim, a lifetime over the 300-second cap, or a replayed jtiFix the assertion. Never retry the same one, because its jti is now in the replay cache.
invalid_grantsub did not resolve to an active user in your organizationConfirm the NinjaTrader user ID. Repeated occurrences are treated as a security signal.
access_deniedThe user revoked your applicationStop issuing assertions for that user and re-establish consent through your own flow. Retrying will not help.
unsupported_grant_typeThe grant_type was not recognizedSend urn:ietf:params:oauth:grant-type:jwt-bearer. Confidential clients sending a client secret get bad_request instead.
bad_requestThe request shape matched no supported grant, such as a missing assertionFix the request.
server_errorA transient server-side failureRetry with exponential backoff, up to about three attempts.

Only server_error is worth retrying. invalid_client, invalid_grant, access_denied, unsupported_grant_type, and bad_request all indicate configuration, consent, or assertion problems that will not resolve on their own. The exact error_description strings are listed on oAuthToken.

Security Practices

  • Store signing keys in a secrets manager and restrict access to the signing service. Never log a private key.
  • Issue one assertion per sign-in and keep exp at 60 seconds. Do not cache assertions for reuse.
  • Serve JWKS over TLS 1.2 or later, use 2048-bit keys or larger, and set Cache-Control: max-age=3600.
  • If a signing key leaks, an attacker can forge assertions for any of your users. NinjaTrader can disable your configuration immediately, after which every assertion returns invalid_client. Access tokens already issued remain valid until they expire, so notify NinjaTrader as soon as you suspect compromise.

Testing

EnvironmentToken endpoint
Staginghttps://live-api.staging.ninjatrader.dev/v1/auth/oauthtoken
Productionhttps://live.tradovateapi.com/v1/auth/oauthtoken

Use separate registrations per environment. Before requesting production enablement, confirm each of these against staging:

CaseExpected
Valid assertion for a registered test userTokens returned, and the API is callable with them
The same assertion submitted twiceThe first succeeds, the second reports a reused jti
Expired assertion, beyond the skew allowanceinvalid_grant reporting an expired assertion
Wrong audinvalid_grant reporting an audience problem
Tampered payloadinvalid_grant reporting an invalid signature
Missing jtiinvalid_grant reporting the missing claim
Unrecognized subinvalid_grant reporting that the user was not found
Key rotationAfter publishing the new key and signing with its kid, the first call refetches JWKS and succeeds
Refresh after sign-inThe standard refresh grant returns a new token pair

When debugging, check the kid in your header against your published JWKS first, since a mismatch is the most common silent failure. Confirm that iat and exp are in seconds rather than milliseconds, and read error_description, which identifies the validation that failed.

Sample Code

Generate a 2048-bit key pair:

$openssl genrsa -out partner-signing-key.pem 2048
$openssl rsa -in partner-signing-key.pem -pubout -out partner-signing-key.pub.pem

Sign an assertion, with PyJWT and cryptography installed:

1import jwt
2import time
3import uuid
4
5with open('partner-signing-key.pem', 'rb') as f:
6 private_key = f.read()
7
8ISSUER = 'https://auth.samplepartner.com'
9AUDIENCE = 'https://live.tradovateapi.com/v1/auth/oauthtoken'
10KID = 'key-2026-01'
11
12def generate_assertion(nt_user_id: int) -> str:
13 now = int(time.time())
14 return jwt.encode(
15 {
16 'iss': ISSUER,
17 'sub': str(nt_user_id),
18 'aud': AUDIENCE,
19 'iat': now,
20 'exp': now + 60,
21 'jti': str(uuid.uuid4()),
22 },
23 private_key,
24 algorithm='RS256',
25 headers={'kid': KID},
26 )
27
28assertion = generate_assertion(123456)
29print(assertion)

Build the JWKS JSON to publish, with cryptography installed:

1from cryptography.hazmat.primitives import serialization
2import base64, json
3
4KID = 'key-2026-01'
5
6with open('partner-signing-key.pem', 'rb') as f:
7 private_key = serialization.load_pem_private_key(f.read(), password=None)
8
9nums = private_key.public_key().public_numbers()
10
11def b64url_uint(value: int) -> str:
12 b = value.to_bytes((value.bit_length() + 7) // 8, 'big')
13 return base64.urlsafe_b64encode(b).decode('ascii').rstrip('=')
14
15jwks = {
16 'keys': [{
17 'kty': 'RSA',
18 'use': 'sig',
19 'alg': 'RS256',
20 'kid': KID,
21 'n': b64url_uint(nums.n),
22 'e': b64url_uint(nums.e),
23 }]
24}
25
26with open('jwks.json', 'w') as f:
27 json.dump(jwks, f, indent=2)
28 f.write('\n')

During rotation, run this for each active key with a distinct kid and combine the resulting keys arrays into one response.

Going Live

  • Production registration created, initially disabled until go-live.
  • JWKS endpoint reachable from the public internet over HTTPS.
  • Production signing key generated, private key secured, public key published.
  • NinjaTrader user IDs obtained and mapped for your first cohort.
  • All staging cases above passing.
  • Error handling implemented for each case, and failed issuance attempts logged on your side for support.
  • Refresh flow integrated, without re-issuing assertions to refresh.
  • Go-live time coordinated with NinjaTrader to enable the production configuration.

Common Questions

A valid assertion returns “User not found”. The ID in sub matches no NinjaTrader user. Confirm the user is provisioned under your organization. Usual causes are a formatting difference, a staging ID used against production, or a user who has not been onboarded.

Does a logout on my platform end the NinjaTrader session? No. Sessions are independent. To end both, call the NinjaTrader logout endpoint with the access token.

Can users still sign in directly? Yes. Partner SSO is enabled per organization and does not block normal sign-in.

How do I report a security issue? Use the security channel established during onboarding. If you suspect key compromise, ask for your configuration to be disabled while you rotate.

Next Steps