Skip to content

OAuth 2.1 Authorization-Code Flow NEW

This is the recommended way for third-party / partner applications to obtain a session token on behalf of an end-user — without ever asking the user to share their API key, API secret, or trading-account password with your app.

The flow follows the standard OAuth 2.1 authorization-code grant (see oauth.net/2.1 for the spec). Your app hands the user off to a SAMCO-hosted consent page; the user authorises the request with the app's API Secret; SAMCO redirects the browser back to your callback URL with a short-lived authorization code; your backend then exchanges that code for an access_token (sent as the x-session-token header on Trade API calls) and a refresh_token.

Two ways to obtain a session token

  • This page — OAuth 2.1 authorization-code flow. Recommended when your app authenticates end-users through a browser (web apps, mobile apps with a webview).
  • Direct (POST /session/token) — For your own apiKey + apiSecret, headless / server-to-server use.

Looking for the SAMCO Web Dashboard?

The SAMCO Web Dashboard (where account holders sign in to create OAuth apps and register static IPs) is documented in the Dashboard User Manual. That dashboard is separate from the partner OAuth consent UI described on this page.


Prerequisites

  1. Create an OAuth app in the Web Dashboard (under API Keys). When creating the app, register your Redirect URL — this is the callback your users will land on after a successful authorisation.
  2. Register static IPs for the app (under Static IPs) if you intend to call POST /oauth/token from a backend server.

Redirect URL rules

  • Must be HTTPS (http://127.0.0.1 is allowed for local testing).
  • The URL your app uses at authorize time must match the redirect URL registered for the app — including scheme, host, port, and path.
  • If you need to change it, edit the app in the dashboard (OTP required).

The flow

 ┌──────────────┐                          ┌─────────────────────────┐
 │ Your App     │ 1. GET /oauth/authorize  │  Samco OAuth Consent UI │
 │              │ ───────────────────────► │  tradeapi.samco.in/app  │
 │              │                          │                         │
 │              │                          │  user enters API Secret │
 │              │                          │                         │
 │              │                          │  POST /oauth/authenticate
 │              │                          │  issues 10-min auth_code│
 │              │                          │  returns redirectTo JSON│
 │              │                          │                         │
 │              │ 2. browser navigates to  │                         │
 │              │    redirect_url?code=…   │                         │
 │              │    &state=…              │                         │
 │              │                          │                         │
 │              │ 3. authorization code    │                         │
 │              │    received — must be    │                         │
 │              │    exchanged within 10m  │                         │
 │              │                          │                         │
 │ Your backend │ 4. POST /oauth/token     │                         │
 │              │    grant_type=           │                         │
 │              │      authorization_code  │                         │
 │              │ ───────────────────────► │  exchange code for      │
 │              │                          │  access_token (24h) +   │
 │              │ 5. {access_token, …}     │  refresh_token (7d)     │
 │              │ ◄─────────────────────── │                         │
 │              │                          │                         │
 │              │ 6. API call with         │                         │
 │              │    x-session-token: …    │                         │
 └──────────────┘                          └─────────────────────────┘

Step 1 — Redirect the user to the SAMCO authorize page

From your app, send the user's browser to:

https://tradeapi.samco.in/app/oauth/authorize
    ?api_key=<AES_ENCRYPTED_API_KEY>
    &redirect_url=https://your-app.example.com/callback
    &state=<OPTIONAL_CSRF_TOKEN>
    &scopes=<COMMA_SEPARATED_SCOPES>   (optional; defaults to "all")
Query paramRequiredDescription
api_keyyesAES-encrypted form of your OAuth app's API key (the value mailed to you when the app was created — paste as-is, do not decrypt).
redirect_urlyesYour callback URL. Must exactly match the redirect URL registered for the app.
statenoAn opaque, unguessable value generated by your app. SAMCO echoes it back unchanged in the callback so you can defend against CSRF.
scopesnoComma-separated list of scopes you want to request. Must be a subset of the scopes registered on the app. Defaults to all. The consent page forwards this to the validation endpoint but does not render it to the end-user in the current build.

Behind the scenes, the page calls GET /oauth/authorize to validate the api_key + redirect_url (+ scopes) against the registered OAuth app. On success the endpoint returns:

json
{
  "status": "Success",
  "message": "Authorization request validated. Continue with POST /oauth/authenticate to complete login and consent.",
  "data": {
    "appName":     "<your app's display name>",
    "apiKey":      "<echoed api_key>",
    "redirectUrl": "<echoed redirect_url>",
    "state":       "<echoed state>",
    "scopes":      "<resolved scopes>",
    "clientUid":   "<owner client UID>",
    "nextAction":  "/oauth/authenticate"
  }
}

The consent UI uses appName to populate the heading. While validation is in flight the user sees a brief loading screen.

If the api_key or redirect_url does not match a registered app (or the app is inactive, or the scopes don't match), the consent UI behaviour splits:

  • If the supplied redirect_url is a valid HTTP(S) URL, the browser is redirected back to it with ?error=invalid_request&errorMessage=...&state=... (see step 3).
  • Otherwise the consent UI displays the error inline and does not redirect.

On successful validation, the user sees the authorisation card with your app name clearly displayed and a single input that asks for the API Secret of that app.

You paste the AES-encrypted API Secret — not the plaintext

The value the consent page accepts is the AES-encrypted API Secret (~96 hex characters) that the dashboard delivered to you on app creation / secret regeneration. Plaintext secrets are rejected by POST /oauth/authenticate. If you no longer have the encrypted value, regenerate it from the API Keys page in the dashboard.

SAMCO OAuth consent page showing the requesting app name and the encrypted API Secret input

The consent page after GET /oauth/authorize validates your api_key and redirect_url.

Consent page with the encrypted API Secret pasted and the Authorize button in its "Authorizing…" submitting state

Submitting calls POST /oauth/authenticate. The button disables and shows "Authorizing…" until the server responds.

Submitting the form calls POST /oauth/authenticate server-side, which runs the following checks in this order:

  1. Decrypt and look up api_key; reject inactive / unknown apps (EOAUTH001).
  2. Validate redirect_url matches the URL registered for the app (EOAUTH002).
  3. Decrypt api_secret and verify its hash against the secret stored when the app was created (EOAUTH008).
  4. Enforce the static IP allowlist if CLIENT_IP_MAPPING rows are registered for this app (EOAUTH009).
  5. Validate the requested scopes are a subset of those registered on the app (EOAUTH003).
  6. Generate a 10-minute, single-use auth_coderandomBytes(32).toString('base64url') (~43 characters) — and persist it.
  7. Return { status: "Success", data: { redirectTo: "<redirect_url>?code=<auth_code>&state=<state>" } }.

The consent UI then navigates the browser to redirectTo on the client side. (Earlier builds of /oauth/authenticate returned an HTTP 302 directly; the current contract returns JSON with data.redirectTo so failures surface inline in the consent UI rather than landing on your callback.)

Treat the encrypted API Secret as a bearer credential

Even though the value the user pastes is already encrypted (not plaintext), anyone who holds it can complete the OAuth flow for your app. Open the consent page only on a trusted machine, over HTTPS, and never paste the value into a shared session. The secret is never persisted client-side — it travels through the browser only on this single submission.

If the user cancels, or any of the checks above fails after the redirect_url itself has been validated, the browser is redirected to the callback URL with an error=...&errorMessage=... instead of a code (see step 3 error variant below).

Step 3 — Browser is redirected back to your callback URL

After a successful authorisation, the browser is redirected to:

https://your-app.example.com/callback
    ?code=<AUTHORIZATION_CODE>
    &state=<ECHOED_STATE>

Browser address bar showing the redirect to https://your-app.example.com/callback?code=…&state=…

What your callback URL looks like in the user's browser after a successful authorization.

Query paramDescription
codeA single-use authorization code — base64url-encoded, ~43 characters, valid for 10 minutes. Exchange it at POST /oauth/token immediately (step 4).
stateThe same state value your app sent in step 1. You must verify it matches what you generated, then discard it (prevents CSRF replay).

If the user cancelled, the redirect carries an error instead of a code:

https://your-app.example.com/callback
    ?error=access_denied
    &errorMessage=User+cancelled+the+login
    &state=<ECHOED_STATE>

If validation in step 1 failed (bad api_key, mismatched redirect_url, inactive app, invalid scopes) and the supplied redirect_url was itself a valid HTTP(S) URL, the consent UI redirects back with:

https://your-app.example.com/callback
    ?error=invalid_request
    &errorMessage=<server-provided+detail>
    &state=<ECHOED_STATE>

If the redirect_url was malformed, the consent UI shows the error inline and does not redirect.

Authorization code received

Your backend must exchange this code at POST /oauth/token (with grant_type=authorization_code) within 10 minutes to obtain an access_token. Use that access_token as the x-session-token header on subsequent Trade API calls.

Step 4 — Exchange the code for an access_token

From your backend (not the browser), POST the code to /oauth/token:

http
POST /oauth/token HTTP/1.1
Host: tradeapi.samco.in
Content-Type: application/json

{
  "grant_type" : "authorization_code",
  "code"       : "<AUTH_CODE_FROM_STEP_3>"
}
bash
curl -X POST 'https://tradeapi.samco.in/oauth/token' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -d '{
    "grant_type": "authorization_code",
    "code": "<AUTH_CODE_FROM_STEP_3>"
  }'
java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class Sample {
  public static void main(String[] args) throws Exception {
    HttpClient client = HttpClient.newHttpClient();

    String requestBody = "{\n" +
        "  \"grant_type\": \"authorization_code\",\n" +
        "  \"code\": \"<AUTH_CODE_FROM_STEP_3>\"\n" +
        "}";

    HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create("https://tradeapi.samco.in/oauth/token"))
        .header("Content-Type", "application/json")
        .header("Accept", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(requestBody))
        .build();

    HttpResponse<String> response =
        client.send(request, HttpResponse.BodyHandlers.ofString());
    System.out.println(response.body());
  }
}
js
(async () => {
  const response = await fetch('https://tradeapi.samco.in/oauth/token', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Accept': 'application/json',
    },
    body: JSON.stringify({
      grant_type: 'authorization_code',
      code: '<AUTH_CODE_FROM_STEP_3>',
    }),
  });

  const data = await response.json();
  console.log(data);
})();
py
import requests

payload = {
  'grant_type': 'authorization_code',
  'code': '<AUTH_CODE_FROM_STEP_3>',
}

r = requests.post('https://tradeapi.samco.in/oauth/token',
  headers={'Content-Type': 'application/json', 'Accept': 'application/json'},
  json=payload)

print(r.json())

Why no api_key / api_secret here

The code itself is the credential. It is unguessable, single-use, valid only for 10 minutes, and bound server-side to the app that authorised it. Re-sending the app credentials at token-exchange time adds no security and is not required.

Response (200):

json
{
  "status": "Success",
  "data": {
    "access_token"             : "eyJhbGciOi...",
    "token_type"               : "Bearer",
    "expires_in"               : 86400,
    "refresh_token"            : "k6Yc7…base64url…",
    "refresh_token_expires_in" : 604800,
    "session_id"               : "9c3f…hex…",
    "user_id"                  : "ABCD1234",
    "scopes"                   : "all",
    "accountID"                : "ABCD1234",
    "accountName"              : "Jane Doe",
    "exchangeList"             : ["NSE", "BSE", "NFO", "CDS", "MCX"],
    "orderTypeList"            : ["LIMIT", "MARKET", "SL", "SL-M"],
    "productList"              : ["CNC", "MIS", "NRML"],
    "srcIp"                    : "203.0.113.42",
    "primaryIp"                : "203.0.113.42",
    "secondaryIp"              : "203.0.113.43"
  }
}
FieldDescription
access_tokenJWT. Send it as the x-session-token header on all subsequent Trade API calls. Valid for 24 hours (expires_in: 86400).
refresh_tokenOpaque base64url string. Use it to obtain a fresh access_token without re-prompting the user. Valid for 7 days (refresh_token_expires_in: 604800).
session_idServer-side identifier for the session (used for revocation / audit).
user_idTrading-account clientUid — same value as accountID.
accountID / accountNameTrading account metadata for the authenticated user.
exchangeList / orderTypeList / productListWhat the account is enabled for.
srcIpThe client IP this token was issued to. Subsequent Trade API calls should originate from this IP.
primaryIp / secondaryIpStatic IPs registered for the OAuth app — useful for verifying your allowlist setup matches what's on file.

The auth code is single-use — replaying it sequentially returns EOAUTH012 and revokes all tokens issued under the same api_key for the user as a security measure. See Concurrent exchanges below for the race-vs-replay distinction.

Step 5 — Use the access token

http
GET /position/getPositions HTTP/1.1
Host: tradeapi.samco.in
Accept: application/json
x-session-token: <ACCESS_TOKEN_FROM_STEP_4>

Step 6 — Refresh the access token (optional)

When access_token is close to expiry (or has expired within the 7-day refresh window), call /oauth/token again with the refresh-token grant:

http
POST /oauth/token HTTP/1.1
Host: tradeapi.samco.in
Content-Type: application/json

{
  "grant_type"   : "refresh_token",
  "refresh_token": "<REFRESH_TOKEN_FROM_STEP_4>"
}
bash
curl -X POST 'https://tradeapi.samco.in/oauth/token' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -d '{
    "grant_type": "refresh_token",
    "refresh_token": "<REFRESH_TOKEN_FROM_STEP_4>"
  }'
java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class Sample {
  public static void main(String[] args) throws Exception {
    HttpClient client = HttpClient.newHttpClient();

    String requestBody = "{\n" +
        "  \"grant_type\": \"refresh_token\",\n" +
        "  \"refresh_token\": \"<REFRESH_TOKEN_FROM_STEP_4>\"\n" +
        "}";

    HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create("https://tradeapi.samco.in/oauth/token"))
        .header("Content-Type", "application/json")
        .header("Accept", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(requestBody))
        .build();

    HttpResponse<String> response =
        client.send(request, HttpResponse.BodyHandlers.ofString());
    System.out.println(response.body());
  }
}
js
(async () => {
  const response = await fetch('https://tradeapi.samco.in/oauth/token', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Accept': 'application/json',
    },
    body: JSON.stringify({
      grant_type: 'refresh_token',
      refresh_token: '<REFRESH_TOKEN_FROM_STEP_4>',
    }),
  });

  const data = await response.json();
  console.log(data);
})();
py
import requests

payload = {
  'grant_type': 'refresh_token',
  'refresh_token': '<REFRESH_TOKEN_FROM_STEP_4>',
}

r = requests.post('https://tradeapi.samco.in/oauth/token',
  headers={'Content-Type': 'application/json', 'Accept': 'application/json'},
  json=payload)

print(r.json())

The response shape is identical to step 4 — a new access_token and a new refresh_token (rotation). The old refresh_token is immediately marked inactive as part of issuing the new one, so always persist the new pair and discard the previous values atomically.

Step 7 — Revoke a token (logout)

http
POST /oauth/revoke HTTP/1.1
Host: tradeapi.samco.in
Content-Type: application/json

{
  "token"     : "<ACCESS_TOKEN_OR_REFRESH_TOKEN>",
  "token_type": "access_token"
}
bash
curl -X POST 'https://tradeapi.samco.in/oauth/revoke' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -d '{
    "token": "<ACCESS_TOKEN_OR_REFRESH_TOKEN>",
    "token_type": "access_token"
  }'
java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class Sample {
  public static void main(String[] args) throws Exception {
    HttpClient client = HttpClient.newHttpClient();

    String requestBody = "{\n" +
        "  \"token\": \"<ACCESS_TOKEN_OR_REFRESH_TOKEN>\",\n" +
        "  \"token_type\": \"access_token\"\n" +
        "}";

    HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create("https://tradeapi.samco.in/oauth/revoke"))
        .header("Content-Type", "application/json")
        .header("Accept", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(requestBody))
        .build();

    HttpResponse<String> response =
        client.send(request, HttpResponse.BodyHandlers.ofString());
    System.out.println(response.body());
  }
}
js
(async () => {
  const response = await fetch('https://tradeapi.samco.in/oauth/revoke', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Accept': 'application/json',
    },
    body: JSON.stringify({
      token: '<ACCESS_TOKEN_OR_REFRESH_TOKEN>',
      token_type: 'access_token',
    }),
  });

  const data = await response.json();
  console.log(data);
})();
py
import requests

payload = {
  'token': '<ACCESS_TOKEN_OR_REFRESH_TOKEN>',
  'token_type': 'access_token',
}

r = requests.post('https://tradeapi.samco.in/oauth/revoke',
  headers={'Content-Type': 'application/json', 'Accept': 'application/json'},
  json=payload)

print(r.json())

Revoking either token invalidates the entire session. The endpoint is idempotent — calling it with an unknown / already-revoked token still returns { "status": "Success", "message": "Token revoked successfully" }.


Sample callback handler

The SAMCO Web Dashboard ships a built-in /callback landing page you can point your test OAuth app at while integrating — register https://<dashboard-host>/callback as your app's redirect_url and the dashboard will render the following after a successful authorisation:

SAMCO dashboard built-in /callback landing page — header "OAuth Callback", green "Authorization code received" panel, code and state fields with Copy buttons, and Java/Node/Python tabbed backend snippets

The page contains, top-to-bottom:

  • A header OAuth Callback with a placeholder description and a Close button (returns to login).
  • A green status panel Authorization code received — verbatim copy: "Your backend must exchange this code at POST /oauth/token (with grant_type=authorization_code) within 10 minutes to obtain an access_token. Use that access_token as the x-session-token header on subsequent Trade API calls."
  • The received code and state values, each with a Copy button.
  • A Backend code samples panel with three tabs — Java, Node.js, Python — each pre-populated with the received code interpolated into a runnable two-step snippet: (1) POST /oauth/token to exchange the code, (2) call GET /holding/getHolding with the resulting access_token in the x-session-token header.
  • A collapsible All query parameters block listing every query-string key/value that landed on the URL — useful for debugging the error variants.
  • A footer reminder: "For production, replace this URL with your own backend endpoint that securely exchanges the code for an access token."

If the redirect carried an error=... instead of a code=..., the green panel is replaced with a red Authorization failed panel that prints the error code and errorMessage, and no code snippets are rendered.

js
// GET https://your-app.example.com/callback?code=...&state=...
app.get('/callback', async (req, res) => {
  const { code, state, error, errorMessage } = req.query;

  // 1. CSRF check
  if (state !== req.session.oauthState) {
    return res.status(400).send('Invalid state');
  }
  delete req.session.oauthState;

  // 2. Handle error
  if (error) {
    return res.status(401).send(`Login failed: ${errorMessage}`);
  }

  // 3. Exchange code for access_token (server-to-server)
  const tokenRes = await fetch('https://tradeapi.samco.in/oauth/token', {
    method:  'POST',
    headers: { 'Content-Type': 'application/json' },
    body:    JSON.stringify({
      grant_type: 'authorization_code',
      code,
    }),
  });
  const { status, data } = await tokenRes.json();
  if (status !== 'Success') return res.status(401).send('Token exchange failed');

  // 4. Persist for the user. Use access_token as x-session-token on Trade APIs.
  req.session.samco = {
    accessToken:  data.access_token,
    refreshToken: data.refresh_token,
    sessionId:    data.session_id,
  };
  res.redirect('/dashboard');
});
py
import os, requests
from flask import request, session, redirect, abort

@app.route('/callback')
def callback():
    if request.args.get('state') != session.pop('oauth_state', None):
        abort(400, 'Invalid state')

    if request.args.get('error'):
        return f"Login failed: {request.args.get('errorMessage')}", 401

    r = requests.post('https://tradeapi.samco.in/oauth/token', json={
        'grant_type': 'authorization_code',
        'code':       request.args['code'],
    }).json()
    if r.get('status') != 'Success':
        return 'Token exchange failed', 401

    d = r['data']
    session['samco_access_token']  = d['access_token']
    session['samco_refresh_token'] = d['refresh_token']
    session['samco_session_id']    = d['session_id']
    return redirect('/dashboard')

Troubleshooting

"Unauthorized - Invalid token. sessId missing." on Trade API calls

You are sending something other than the access_token from step 4 as the x-session-token header. The code from the callback URL is not a session token — it must first be exchanged at POST /oauth/token (step 4). Use the access_token field of that response as x-session-token.

"Authorization code already used. All tokens revoked for security."

The code returned in step 3 is single-use. If you call POST /oauth/token twice with the same code (or your callback fires twice), SAMCO treats it as a replay and revokes every token previously issued under the same api_key for the user. Re-run the full flow from step 1.

"Authorization code has expired" (EOAUTH013)

code is valid for 10 minutes. Make sure your callback exchanges it promptly; don't queue it for batch processing.

"Concurrent token exchange in progress. Please retry." (EOAUTH030)

You issued two parallel POST /oauth/token requests with the same code (often a runaway retry library, or the user double-clicking through your callback). The backend uses a compare-and-swap on the code's used flag — the winner gets tokens, the loser gets EOAUTH030. Wait briefly, then re-issue once; if the original request actually succeeded, the retry will now surface EOAUTH012 ("already used") because the code has been claimed.

Retrying /oauth/token safely

  • Sequential retries inside the 10-minute window are safe as long as the previous attempt did not succeed. A genuine transient infrastructure error (network blip, upstream 5xx before the code was claimed) leaves the code intact.
  • Concurrent retries can race and produce EOAUTH030 — serialize them.
  • Any EOAUTH012 response means the code was already redeemed and all tokens issued under your api_key for that user have been revoked. Re-run the full authorization flow from step 1.

"Unable to start your trading session" after entering the API Secret

OAuth credentials were accepted but our trading backend could not establish a session for the underlying SAMCO trading account that owns the OAuth app. The integrator (or the account holder) should:

  1. Open the SAMCO mobile app (or Samco Web) and sign in once with the SAMCO Client ID + password (and OTP).
  2. If the password is rejected, complete a password reset from the mobile app's Forgot Password flow.
  3. If the account shows as blocked / dormant / under review, contact support@samco.in to unblock / reactivate.
  4. Retry the OAuth login.

This is also surfaced by the direct POST /session/token endpoint with the same message body.

Error code reference

The error codes you'll realistically encounter at runtime, mapped to the step that emits them:

CodeEmitted byMeaning
EOAUTH001/oauth/authorize, /authenticateInvalid or inactive api_key (decrypt failure or app not Active).
EOAUTH002/oauth/authorize, /authenticateredirect_url does not match the URL registered for the app.
EOAUTH003/oauth/authorize, /authenticateRequested scopes are not a subset of those registered on the app.
EOAUTH008/oauth/authenticateInvalid api_secret (decrypt failure or hash mismatch).
EOAUTH009/oauth/authenticate, /tokenRequest originated from an IP not on the app's static-IP allowlist.
EOAUTH010/oauth/tokencode field missing on authorization_code grant.
EOAUTH011/oauth/tokencode not found in the auth-codes store.
EOAUTH012/oauth/tokencode was already used — all tokens for this api_key revoked.
EOAUTH013/oauth/tokencode expired (older than 10 minutes).
EOAUTH015/oauth/tokenrefresh_token field missing on refresh_token grant.
EOAUTH016/oauth/tokenrefresh_token not found or has been invalidated.
EOAUTH017/oauth/tokenrefresh_token expired (older than 7 days).
EOAUTH030/oauth/tokenConcurrent exchange of the same code — see above.
EOAUTH999/oauth/tokenTrading backend could not establish a session for the underlying account (see "Unable to start your trading session" above).

Why this flow?

  • No credential sharing — end-users never hand their SAMCO password or your app's API secret to a third-party UI.
  • Standard OAuth 2.1 — well-understood authorization-code grant with replay protection on the code.
  • SEBI compliant — authorisation happens on SAMCO's domain with full audit trail.
  • Static-IP-friendly — the token-exchange endpoint enforces the IPs registered for your OAuth app; calls from other IPs are rejected.
  • Long-lived sessions — 24-hour access_token + 7-day rotating refresh_token keeps integrations stable without forcing daily re-consent.

Need a backend-only flow?

If your integration is purely server-to-server (no end-user browser involved), use POST /session/token directly with your own apiKey + apiSecret.