> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://partner.ninjatrader.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://partner.ninjatrader.com/_mcp/server.

# Partner Token

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`](/connect/api/rest-api-endpoints/authentication/o-auth-token) 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.

| Field         | Description                                                                                                         |
| ------------- | ------------------------------------------------------------------------------------------------------------------- |
| Platform name | The name shown in any NinjaTrader interface that mentions your platform                                             |
| `iss`         | The exact value your assertions will carry in the `iss` claim                                                       |
| JWKS URL      | An HTTPS URL where NinjaTrader can fetch your JWKS                                                                  |
| `aud`         | The 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.

```json
{
  "alg": "RS256",
  "kid": "key-2026-01"
}
```

Required claims:

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

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`](/connect/api/rest-api-endpoints/authentication/o-auth-token).

## 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](https://datatracker.ietf.org/doc/html/rfc7517).

```json
{
  "keys": [
    {
      "kty": "RSA",
      "use": "sig",
      "alg": "RS256",
      "kid": "key-2026-01",
      "n": "0vx7agoebGcQSuuPiLJXZptN9nndr…",
      "e": "AQAB"
    }
  ]
}
```

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

```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\":\"$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:

```json
{
  "access_token": "…",
  "refresh_token": "…",
  "token_type": "bearer",
  "expires_in": 86400,
  "refresh_token_expires_in": 93600
}
```

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:

```json
{
  "grant_type": "refresh_token",
  "refresh_token": "<previously-issued>"
}
```

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.

```json
{
  "error": "invalid_grant",
  "error_description": "User not found: userId=1234"
}
```

| `error`                  | What it means                                                                                                                                     | What to do                                                                                                                  |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `invalid_client`         | Your `iss` is not registered, or your configuration is disabled or archived                                                                       | Contact NinjaTrader. Your registration may need updating.                                                                   |
| `invalid_grant`          | The assertion failed validation: expired, bad signature, wrong audience, a missing claim, a lifetime over the 300-second cap, or a replayed `jti` | Fix the assertion. Never retry the same one, because its `jti` is now in the replay cache.                                  |
| `invalid_grant`          | `sub` did not resolve to an active user in your organization                                                                                      | Confirm the NinjaTrader user ID. Repeated occurrences are treated as a security signal.                                     |
| `access_denied`          | The user revoked your application                                                                                                                 | Stop issuing assertions for that user and re-establish consent through your own flow. Retrying will not help.               |
| `unsupported_grant_type` | The `grant_type` was not recognized                                                                                                               | Send `urn:ietf:params:oauth:grant-type:jwt-bearer`. Confidential clients sending a client secret get `bad_request` instead. |
| `bad_request`            | The request shape matched no supported grant, such as a missing `assertion`                                                                       | Fix the request.                                                                                                            |
| `server_error`           | A transient server-side failure                                                                                                                   | Retry 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`](/connect/api/rest-api-endpoints/authentication/o-auth-token).

## 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

| Environment | Token endpoint                                                |
| ----------- | ------------------------------------------------------------- |
| Staging     | `https://live-api.staging.ninjatrader.dev/v1/auth/oauthtoken` |
| Production  | `https://live.tradovateapi.com/v1/auth/oauthtoken`            |

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

| Case                                         | Expected                                                                                            |
| -------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| Valid assertion for a registered test user   | Tokens returned, and the API is callable with them                                                  |
| The same assertion submitted twice           | The first succeeds, the second reports a reused `jti`                                               |
| Expired assertion, beyond the skew allowance | `invalid_grant` reporting an expired assertion                                                      |
| Wrong `aud`                                  | `invalid_grant` reporting an audience problem                                                       |
| Tampered payload                             | `invalid_grant` reporting an invalid signature                                                      |
| Missing `jti`                                | `invalid_grant` reporting the missing claim                                                         |
| Unrecognized `sub`                           | `invalid_grant` reporting that the user was not found                                               |
| Key rotation                                 | After publishing the new key and signing with its `kid`, the first call refetches JWKS and succeeds |
| Refresh after sign-in                        | The 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:

```bash
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:

```python
import jwt
import time
import uuid

with open('partner-signing-key.pem', 'rb') as f:
    private_key = f.read()

ISSUER   = 'https://auth.samplepartner.com'
AUDIENCE = 'https://live.tradovateapi.com/v1/auth/oauthtoken'
KID      = 'key-2026-01'

def generate_assertion(nt_user_id: int) -> str:
    now = int(time.time())
    return jwt.encode(
        {
            'iss': ISSUER,
            'sub': str(nt_user_id),
            'aud': AUDIENCE,
            'iat': now,
            'exp': now + 60,
            'jti': str(uuid.uuid4()),
        },
        private_key,
        algorithm='RS256',
        headers={'kid': KID},
    )

assertion = generate_assertion(123456)
print(assertion)
```

Build the JWKS JSON to publish, with `cryptography` installed:

```python
from cryptography.hazmat.primitives import serialization
import base64, json

KID = 'key-2026-01'

with open('partner-signing-key.pem', 'rb') as f:
    private_key = serialization.load_pem_private_key(f.read(), password=None)

nums = private_key.public_key().public_numbers()

def b64url_uint(value: int) -> str:
    b = value.to_bytes((value.bit_length() + 7) // 8, 'big')
    return base64.urlsafe_b64encode(b).decode('ascii').rstrip('=')

jwks = {
    'keys': [{
        'kty': 'RSA',
        'use': 'sig',
        'alg': 'RS256',
        'kid': KID,
        'n': b64url_uint(nums.n),
        'e': b64url_uint(nums.e),
    }]
}

with open('jwks.json', 'w') as f:
    json.dump(jwks, f, indent=2)
    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

* [Embedding the Funding Page with an iframe](/connect/overview/partner-integration/embedding-funding-page-with-i-frame): put a NinjaTrader-hosted funding page inside your own application.
* [SSO with ShortGrantCode](/connect/overview/partner-integration/sso-with-short-grant-code): redirect a signed-in user to a NinjaTrader-hosted page.