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

# Exchange Short Grant Code

POST https://live.tradovateapi.com/v1/auth/exchangeshortgrantcode
Content-Type: application/json

### Redeem a short grant code for a session.

**Available to:** Anonymous callers, from a NinjaTrader-owned origin

**Environments:** Live. The endpoint is not blocked on Demo, but it only ever redeems codes minted in partner mode, which depends on a partner configuration provisioned on Live.

**[Rate Limit](/overview/core-concepts/rate-limits):** 10 requests per hour, 3-second back-off, counts failed requests only

**Partners do not normally call this endpoint.** It is the other half of the [`shortGrantCode`](/api/rest-api-endpoints/authentication/short-grant-code) handoff, and it is called by the NinjaTrader-hosted application that receives your redirect: the application reads the code from the URL, exchanges it here for a real access token, and the user lands signed in. It is documented so you can reason about the full round trip and interpret what a user sees when a handoff fails.

<Warning>**Warning:** This endpoint returns **HTTP 200** for successes **and** business errors. Test for success by checking both `errorText` and whether `accessToken` is present: a restricted session returns an access token alongside a non-empty `errorText` carrying a warning, so `errorText` alone misreads that case as a failure.</Warning>

**How the Code Is Validated**

Four checks run in order, and the first failure ends the request:

1. **Origin.** The request must come from a NinjaTrader-owned origin: a `tradovate.com`, `ninjatrader.com`, or `ninjatrader.dev` host. This is why the exchange is performed by the NinjaTrader application rather than by your own page.
2. **The code itself.** Codes are single-use: redemption deletes the stored code, so a second attempt with the same value fails. Codes also expire on the lifetime returned as `expires_in` when they were minted, and a newer code for the same user supersedes an older one.
3. **Client IP.** The resolved client IP must match the `expectedClientIp` the code was bound to.
4. **The user.** The account is re-validated at redemption: it must be an active trader account that is not an organization administrator, and it must still belong to the partner organization the code was minted for.

All four failure modes surface as `errorText` values on an `HTTP 200` response. See the table below.

<Warning>**Warning:** A code that reaches step 2 is consumed even when the exchange then fails. Retrying the same code after an IP or account failure returns `"Invalid code"` rather than the original reason, which hides the real cause. Mint a fresh code for every attempt.</Warning>

Because the IP comparison is an exact string match, two textual forms of the same IPv6 address do not match. Emit `expectedClientIp` in the same form the client will present at redemption.

**Field Details**

`code` is the value returned by [`shortGrantCode`](/api/rest-api-endpoints/authentication/short-grant-code). `appId` and `appVersion` identify the application redeeming the code and are required. The optional `deviceId` feeds device-trust evaluation on the resulting session; when it is omitted, the device recorded at the time the code was minted is used instead.

**Response Fields**

A successful exchange returns the same `AccessTokenResponse` payload as a normal sign-in: `accessToken`, `mdAccessToken`, `expirationTime`, `userId`, `name`, and the account-status flags.

<Info>There is no refresh token on this response. The session is renewed the same way any other session is: with [`renewAccessToken`](/api/rest-api-endpoints/authentication/renew-access-token) before `expirationTime` elapses.</Info>

**Sample Call**

```bash
curl -X POST "https://live.tradovateapi.com/v1/auth/exchangeshortgrantcode" \
  -H "Content-Type: application/json" \
  -d '{
        "code": "<code from /auth/shortgrantcode>",
        "appId": "NinjaTrader Web",
        "appVersion": "1.0",
        "deviceId": "<client device id>"
      }'
```

```json
{
  "accessToken": "…",
  "mdAccessToken": "…",
  "expirationTime": "2026-08-05T18:20:00.000Z",
  "userId": 12345,
  "name": "trader-name"
}
```

**Common Failure Scenarios**

- The code was already redeemed, expired, or was superseded by a newer code for the same user.
- The user's IP at redemption differs from the `expectedClientIp` the code was bound to.
- The request originated from a host outside the NinjaTrader-owned origins.
- The account is inactive, is an organization administrator, or is no longer in the partner's organization.
- The partner configuration was disabled or archived between minting and redemption.
- Repeated failures tripped the rate limit, which responds with a time-penalty body rather than an HTTP error.

**Error Messages**

| `errorText` | Trigger |
|-------------|---------|
| `"Invalid code"` | The code is unrecognized, expired, already redeemed, or superseded by a newer code. All four cases share this message. |
| `"Access denied from another IP"` | The resolved client IP does not match the code's `expectedClientIp` |
| `"Access denied for <origin>"` | The request came from an origin outside the allowed NinjaTrader domains |
| `"This functionality is not available to administrators"` | The account is an organization administrator or not a trader |
| `"This functionality is not available to locked users"` | The account is inactive |
| `"Access denied"` | A partner-policy rejection: the configuration is disabled or archived, the user is no longer bound to the partner's organization, the provider key is unknown, or mutual-TLS verification failed |

Device-trust evaluation also runs on the resulting session, so a user with two-factor authentication enabled can be denied at redemption for an untrusted device or a new machine. Those denials carry their own `errorText` values, which are shared with the normal sign-in flow rather than specific to this endpoint.

Reference: https://partner.ninjatrader.com/connect/api/rest-api-endpoints/authentication/exchange-short-grant-code

## OpenAPI Specification

```yaml
openapi: 3.1.0
info:
  title: connect
  version: 1.0.0
paths:
  /auth/exchangeshortgrantcode:
    post:
      operationId: exchange-short-grant-code
      summary: Exchange Short Grant Code
      description: >-
        ### Redeem a short grant code for a session.


        **Available to:** Anonymous callers, from a NinjaTrader-owned origin


        **Environments:** Live. The endpoint is not blocked on Demo, but it only
        ever redeems codes minted in partner mode, which depends on a partner
        configuration provisioned on Live.


        **[Rate Limit](/overview/core-concepts/rate-limits):** 10 requests per
        hour, 3-second back-off, counts failed requests only


        **Partners do not normally call this endpoint.** It is the other half of
        the
        [`shortGrantCode`](/api/rest-api-endpoints/authentication/short-grant-code)
        handoff, and it is called by the NinjaTrader-hosted application that
        receives your redirect: the application reads the code from the URL,
        exchanges it here for a real access token, and the user lands signed in.
        It is documented so you can reason about the full round trip and
        interpret what a user sees when a handoff fails.


        <Warning>**Warning:** This endpoint returns **HTTP 200** for successes
        **and** business errors. Test for success by checking both `errorText`
        and whether `accessToken` is present: a restricted session returns an
        access token alongside a non-empty `errorText` carrying a warning, so
        `errorText` alone misreads that case as a failure.</Warning>


        **How the Code Is Validated**


        Four checks run in order, and the first failure ends the request:


        1. **Origin.** The request must come from a NinjaTrader-owned origin: a
        `tradovate.com`, `ninjatrader.com`, or `ninjatrader.dev` host. This is
        why the exchange is performed by the NinjaTrader application rather than
        by your own page.

        2. **The code itself.** Codes are single-use: redemption deletes the
        stored code, so a second attempt with the same value fails. Codes also
        expire on the lifetime returned as `expires_in` when they were minted,
        and a newer code for the same user supersedes an older one.

        3. **Client IP.** The resolved client IP must match the
        `expectedClientIp` the code was bound to.

        4. **The user.** The account is re-validated at redemption: it must be
        an active trader account that is not an organization administrator, and
        it must still belong to the partner organization the code was minted
        for.


        All four failure modes surface as `errorText` values on an `HTTP 200`
        response. See the table below.


        <Warning>**Warning:** A code that reaches step 2 is consumed even when
        the exchange then fails. Retrying the same code after an IP or account
        failure returns `"Invalid code"` rather than the original reason, which
        hides the real cause. Mint a fresh code for every attempt.</Warning>


        Because the IP comparison is an exact string match, two textual forms of
        the same IPv6 address do not match. Emit `expectedClientIp` in the same
        form the client will present at redemption.


        **Field Details**


        `code` is the value returned by
        [`shortGrantCode`](/api/rest-api-endpoints/authentication/short-grant-code).
        `appId` and `appVersion` identify the application redeeming the code and
        are required. The optional `deviceId` feeds device-trust evaluation on
        the resulting session; when it is omitted, the device recorded at the
        time the code was minted is used instead.


        **Response Fields**


        A successful exchange returns the same `AccessTokenResponse` payload as
        a normal sign-in: `accessToken`, `mdAccessToken`, `expirationTime`,
        `userId`, `name`, and the account-status flags.


        <Info>There is no refresh token on this response. The session is renewed
        the same way any other session is: with
        [`renewAccessToken`](/api/rest-api-endpoints/authentication/renew-access-token)
        before `expirationTime` elapses.</Info>


        **Sample Call**


        ```bash

        curl -X POST
        "https://live.tradovateapi.com/v1/auth/exchangeshortgrantcode" \
          -H "Content-Type: application/json" \
          -d '{
                "code": "<code from /auth/shortgrantcode>",
                "appId": "NinjaTrader Web",
                "appVersion": "1.0",
                "deviceId": "<client device id>"
              }'
        ```


        ```json

        {
          "accessToken": "…",
          "mdAccessToken": "…",
          "expirationTime": "2026-08-05T18:20:00.000Z",
          "userId": 12345,
          "name": "trader-name"
        }

        ```


        **Common Failure Scenarios**


        - The code was already redeemed, expired, or was superseded by a newer
        code for the same user.

        - The user's IP at redemption differs from the `expectedClientIp` the
        code was bound to.

        - The request originated from a host outside the NinjaTrader-owned
        origins.

        - The account is inactive, is an organization administrator, or is no
        longer in the partner's organization.

        - The partner configuration was disabled or archived between minting and
        redemption.

        - Repeated failures tripped the rate limit, which responds with a
        time-penalty body rather than an HTTP error.


        **Error Messages**


        | `errorText` | Trigger |

        |-------------|---------|

        | `"Invalid code"` | The code is unrecognized, expired, already
        redeemed, or superseded by a newer code. All four cases share this
        message. |

        | `"Access denied from another IP"` | The resolved client IP does not
        match the code's `expectedClientIp` |

        | `"Access denied for <origin>"` | The request came from an origin
        outside the allowed NinjaTrader domains |

        | `"This functionality is not available to administrators"` | The
        account is an organization administrator or not a trader |

        | `"This functionality is not available to locked users"` | The account
        is inactive |

        | `"Access denied"` | A partner-policy rejection: the configuration is
        disabled or archived, the user is no longer bound to the partner's
        organization, the provider key is unknown, or mutual-TLS verification
        failed |


        Device-trust evaluation also runs on the resulting session, so a user
        with two-factor authentication enabled can be denied at redemption for
        an untrusted device or a new machine. Those denials carry their own
        `errorText` values, which are shared with the normal sign-in flow rather
        than specific to this endpoint.
      tags:
        - Authentication
      responses:
        '200':
          description: AccessTokenResponse
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccessTokenResponse'
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ExchangeShortGrantCode'
servers:
  - url: https://live.tradovateapi.com/v1
    description: Live
components:
  schemas:
    ExchangeShortGrantCode:
      type: object
      properties:
        code:
          type: string
        appId:
          type: string
        appVersion:
          type: string
        deviceId:
          type: string
      required:
        - code
        - appId
        - appVersion
      title: ExchangeShortGrantCode
    AccessTokenResponseHibpHint:
      type: string
      enum:
        - EmailAndPasswordCompromised
        - PasswordCompromised
      description: EmailAndPasswordCompromised, PasswordCompromised
      title: AccessTokenResponseHibpHint
    AccessTokenResponseUserStatus:
      type: string
      enum:
        - Active
        - Closed
        - Initiated
        - TemporaryLocked
        - UnconfirmedEmail
      description: Active, Closed, Initiated, TemporaryLocked, UnconfirmedEmail
      title: AccessTokenResponseUserStatus
    AccessTokenResponse:
      type: object
      properties:
        errorText:
          type: string
          description: Non-empty if the request failed
        hibpHint:
          $ref: '#/components/schemas/AccessTokenResponseHibpHint'
          description: EmailAndPasswordCompromised, PasswordCompromised
        accessToken:
          type: string
        expirationTime:
          type: string
          format: date-time
        passwordExpirationTime:
          type: string
          format: date-time
        userStatus:
          $ref: '#/components/schemas/AccessTokenResponseUserStatus'
          description: Active, Closed, Initiated, TemporaryLocked, UnconfirmedEmail
        userId:
          type: integer
          format: int64
        name:
          type: string
        hasLive:
          type: boolean
        hasSimPlus:
          type: boolean
        showKIDs:
          type: boolean
      title: AccessTokenResponse

```

## Examples



**Request**

```json
{
  "code": "string",
  "appId": "string",
  "appVersion": "string"
}
```

**Response**

```json
{
  "errorText": "string",
  "hibpHint": "EmailAndPasswordCompromised",
  "accessToken": "string",
  "expirationTime": "2024-01-15T09:30:00Z",
  "passwordExpirationTime": "2024-01-15T09:30:00Z",
  "userStatus": "Active",
  "userId": 1,
  "name": "string",
  "hasLive": true,
  "hasSimPlus": true,
  "showKIDs": true
}
```

**SDK Code**

```python
import requests

url = "https://live.tradovateapi.com/v1/auth/exchangeshortgrantcode"

payload = {
    "code": "string",
    "appId": "string",
    "appVersion": "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/exchangeshortgrantcode';
const options = {
  method: 'POST',
  headers: {'Content-Type': 'application/json'},
  body: '{"code":"string","appId":"string","appVersion":"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/exchangeshortgrantcode"

	payload := strings.NewReader("{\n  \"code\": \"string\",\n  \"appId\": \"string\",\n  \"appVersion\": \"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/exchangeshortgrantcode")

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  \"code\": \"string\",\n  \"appId\": \"string\",\n  \"appVersion\": \"string\"\n}"

response = http.request(request)
puts response.read_body
```

```java
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.post("https://live.tradovateapi.com/v1/auth/exchangeshortgrantcode")
  .header("Content-Type", "application/json")
  .body("{\n  \"code\": \"string\",\n  \"appId\": \"string\",\n  \"appVersion\": \"string\"\n}")
  .asString();
```

```php
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('POST', 'https://live.tradovateapi.com/v1/auth/exchangeshortgrantcode', [
  'body' => '{
  "code": "string",
  "appId": "string",
  "appVersion": "string"
}',
  'headers' => [
    'Content-Type' => 'application/json',
  ],
]);

echo $response->getBody();
```

```csharp
using RestSharp;

var client = new RestClient("https://live.tradovateapi.com/v1/auth/exchangeshortgrantcode");
var request = new RestRequest(Method.POST);
request.AddHeader("Content-Type", "application/json");
request.AddParameter("application/json", "{\n  \"code\": \"string\",\n  \"appId\": \"string\",\n  \"appVersion\": \"string\"\n}", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
```

```swift
import Foundation

let headers = ["Content-Type": "application/json"]
let parameters = [
  "code": "string",
  "appId": "string",
  "appVersion": "string"
] as [String : Any]

let postData = JSONSerialization.data(withJSONObject: parameters, options: [])

let request = NSMutableURLRequest(url: NSURL(string: "https://live.tradovateapi.com/v1/auth/exchangeshortgrantcode")! 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()
```