API Authentication Guide

Simplified authentication for OptionsPlay® RESTful APIs

Overview

OptionsPlay is introducing a streamlined authentication method for our RESTful APIs. The new approach eliminates the need for OAuth token management, reducing your integration complexity from multiple steps to a single API key.

📋 Both Methods Supported

The legacy OAuth approach continues to work. You can migrate at your convenience—there's no deadline to switch. However, we recommend the new approach for simpler integration and reduced maintenance.

What's Changing

sequenceDiagram participant C as Your Application participant A as OptionsPlay APIs rect rgb(240, 240, 240) Note over C,A: Legacy Approach (2 steps) C->>A: POST /token (clientId + clientSecret) A-->>C: Bearer Token (expires in ~60 min) C->>A: GET /api/... + Authorization: Bearer {token} A-->>C: Response end rect rgb(230, 244, 234) Note over C,A: New Approach (1 step) C->>A: GET /api/... + Op-Subscription-Key: {apiKey} A-->>C: Response end

The new approach handles OAuth behind the scenes. You simply include your subscription key with each request—no token acquisition, no token refresh, no expiration handling.

Quick Comparison

Legacy Approach
  • Two credentials (clientId + clientSecret)
  • Requires token endpoint call
  • Tokens expire (~60 minutes)
  • You manage token refresh logic
  • Base URL: app.optionsplay.com
  • Header: Authorization: Bearer ...
✓ New Approach
  • Single API key
  • No token endpoint needed
  • Key doesn't expire
  • No refresh logic required
  • Base URL: apis.optionsplay.com
  • Header: Op-Subscription-Key: ...
Aspect Legacy New
Credentials clientId + clientSecret Single API key
Base URL https://app.optionsplay.com https://apis.optionsplay.com
Auth Header Authorization: Bearer {token} Op-Subscription-Key: {apiKey}
Token Management Required (acquire + refresh) Not needed
Usage Analytics Limited Full portal access

New Approach: Subscription Key

With the new approach, include your subscription key in the Op-Subscription-Key header with every request. That's it.

Request Format

curl -X GET "https://apis.optionsplay.com/api/v1/platform/why/AAPL" \
  -H "Op-Subscription-Key: YOUR_API_KEY"

Authentication Flow

sequenceDiagram participant App as Your Application participant APIM as OptionsPlay API Gateway participant API as OptionsPlay Backend App->>APIM: Request + Op-Subscription-Key APIM->>APIM: Validate subscription key APIM->>APIM: Acquire OAuth token (cached) APIM->>API: Request + Bearer token API-->>APIM: Response APIM-->>App: Response Note over APIM: Token caching & refresh
handled automatically
✓ No Code Changes for Token Refresh

The API gateway manages OAuth tokens internally. Your code simply sends the subscription key—no need to handle token expiration, refresh logic, or retry mechanisms.

Legacy Approach: OAuth Bearer Token

The legacy approach remains fully supported. If you have an existing integration, it will continue to work without changes.

Step 1: Acquire Token

curl -X POST "https://app.optionsplay.com/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET"

Step 2: Use Token

curl -X GET "https://app.optionsplay.com/api/v1/platform/why/AAPL" \
  -H "Authorization: Bearer {access_token}"

Token Lifecycle

sequenceDiagram participant App as Your Application participant Auth as Token Endpoint participant API as OptionsPlay API App->>Auth: POST /token (credentials) Auth-->>App: access_token (expires_in: 3600) loop Every API Call App->>App: Check token expiration alt Token Valid App->>API: Request + Bearer token API-->>App: Response else Token Expired App->>Auth: POST /token (credentials) Auth-->>App: New access_token App->>API: Request + Bearer token API-->>App: Response end end

Migration Steps

Migrating to the new approach takes just a few minutes:

Get Your Subscription Key

Contact your OptionsPlay account representative or email support@optionsplay.com to receive your new API subscription key.

Update Base URL

Change your API base URL from app.optionsplay.com to apis.optionsplay.com. The API paths remain the same.

Replace Auth Header

Replace Authorization: Bearer {token} with Op-Subscription-Key: {apiKey} in your requests.

Remove Token Logic

Delete your token acquisition and refresh code. It's no longer needed—enjoy the simpler codebase!

Code Examples

Before (Legacy)

# Step 1: Get token
TOKEN=$(curl -s -X POST "https://app.optionsplay.com/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET" | jq -r '.access_token')

# Step 2: Call API
curl -X GET "https://app.optionsplay.com/api/v1/platform/why/AAPL" \
  -H "Authorization: Bearer $TOKEN"
import requests
from datetime import datetime, timedelta

class OptionsPlayClient:
    def __init__(self, client_id, client_secret):
        self.client_id = client_id
        self.client_secret = client_secret
        self.base_url = "https://app.optionsplay.com"
        self.token = None
        self.token_expiry = None
    
    def _get_token(self):
        """Acquire or refresh OAuth token"""
        if self.token and self.token_expiry > datetime.now():
            return self.token
        
        response = requests.post(
            f"{self.base_url}/token",
            data={
                "grant_type": "client_credentials",
                "client_id": self.client_id,
                "client_secret": self.client_secret
            }
        )
        response.raise_for_status()
        data = response.json()
        
        self.token = data["access_token"]
        self.token_expiry = datetime.now() + timedelta(seconds=data["expires_in"] - 60)
        return self.token
    
    def get_why(self, symbol):
        """Get trade ideas for a symbol"""
        token = self._get_token()
        response = requests.get(
            f"{self.base_url}/api/v1/platform/why/{symbol}",
            headers={"Authorization": f"Bearer {token}"}
        )
        response.raise_for_status()
        return response.json()

# Usage
client = OptionsPlayClient("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET")
data = client.get_why("AAPL")
class OptionsPlayClient {
  constructor(clientId, clientSecret) {
    this.clientId = clientId;
    this.clientSecret = clientSecret;
    this.baseUrl = 'https://app.optionsplay.com';
    this.token = null;
    this.tokenExpiry = null;
  }

  async getToken() {
    // Return cached token if still valid
    if (this.token && this.tokenExpiry > Date.now()) {
      return this.token;
    }

    const response = await fetch(`${this.baseUrl}/token`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams({
        grant_type: 'client_credentials',
        client_id: this.clientId,
        client_secret: this.clientSecret
      })
    });

    if (!response.ok) throw new Error('Token acquisition failed');
    
    const data = await response.json();
    this.token = data.access_token;
    this.tokenExpiry = Date.now() + (data.expires_in - 60) * 1000;
    return this.token;
  }

  async getWhy(symbol) {
    const token = await this.getToken();
    const response = await fetch(
      `${this.baseUrl}/api/v1/platform/why/${symbol}`,
      { headers: { 'Authorization': `Bearer ${token}` } }
    );
    
    if (!response.ok) throw new Error('API request failed');
    return response.json();
  }
}

// Usage
const client = new OptionsPlayClient('YOUR_CLIENT_ID', 'YOUR_CLIENT_SECRET');
const data = await client.getWhy('AAPL');
public class OptionsPlayClient
{
    private readonly string _clientId;
    private readonly string _clientSecret;
    private readonly string _baseUrl = "https://app.optionsplay.com";
    private readonly HttpClient _httpClient;
    private string _token;
    private DateTime _tokenExpiry;

    public OptionsPlayClient(string clientId, string clientSecret)
    {
        _clientId = clientId;
        _clientSecret = clientSecret;
        _httpClient = new HttpClient();
    }

    private async Task<string> GetTokenAsync()
    {
        if (_token != null && _tokenExpiry > DateTime.UtcNow)
            return _token;

        var content = new FormUrlEncodedContent(new[]
        {
            new KeyValuePair<string, string>("grant_type", "client_credentials"),
            new KeyValuePair<string, string>("client_id", _clientId),
            new KeyValuePair<string, string>("client_secret", _clientSecret)
        });

        var response = await _httpClient.PostAsync($"{_baseUrl}/token", content);
        response.EnsureSuccessStatusCode();

        var json = await response.Content.ReadAsStringAsync();
        var tokenResponse = JsonSerializer.Deserialize<TokenResponse>(json);

        _token = tokenResponse.AccessToken;
        _tokenExpiry = DateTime.UtcNow.AddSeconds(tokenResponse.ExpiresIn - 60);
        return _token;
    }

    public async Task<string> GetWhyAsync(string symbol)
    {
        var token = await GetTokenAsync();
        _httpClient.DefaultRequestHeaders.Authorization = 
            new AuthenticationHeaderValue("Bearer", token);

        var response = await _httpClient.GetAsync(
            $"{_baseUrl}/api/v1/platform/why/{symbol}");
        response.EnsureSuccessStatusCode();

        return await response.Content.ReadAsStringAsync();
    }
}

// Usage
var client = new OptionsPlayClient("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET");
var data = await client.GetWhyAsync("AAPL");

After (New Approach)

# Single step - no token needed!
curl -X GET "https://apis.optionsplay.com/api/v1/platform/why/AAPL" \
  -H "Op-Subscription-Key: YOUR_API_KEY"
import requests

class OptionsPlayClient:
    def __init__(self, api_key):
        self.api_key = api_key
        self.base_url = "https://apis.optionsplay.com"
    
    def get_why(self, symbol):
        """Get trade ideas for a symbol"""
        response = requests.get(
            f"{self.base_url}/api/v1/platform/why/{symbol}",
            headers={"Op-Subscription-Key": self.api_key}
        )
        response.raise_for_status()
        return response.json()

# Usage - that's it!
client = OptionsPlayClient("YOUR_API_KEY")
data = client.get_why("AAPL")
class OptionsPlayClient {
  constructor(apiKey) {
    this.apiKey = apiKey;
    this.baseUrl = 'https://apis.optionsplay.com';
  }

  async getWhy(symbol) {
    const response = await fetch(
      `${this.baseUrl}/api/v1/platform/why/${symbol}`,
      { headers: { 'Op-Subscription-Key': this.apiKey } }
    );
    
    if (!response.ok) throw new Error('API request failed');
    return response.json();
  }
}

// Usage - that's it!
const client = new OptionsPlayClient('YOUR_API_KEY');
const data = await client.getWhy('AAPL');
public class OptionsPlayClient
{
    private readonly string _apiKey;
    private readonly string _baseUrl = "https://apis.optionsplay.com";
    private readonly HttpClient _httpClient;

    public OptionsPlayClient(string apiKey)
    {
        _apiKey = apiKey;
        _httpClient = new HttpClient();
        _httpClient.DefaultRequestHeaders.Add("Op-Subscription-Key", _apiKey);
    }

    public async Task<string> GetWhyAsync(string symbol)
    {
        var response = await _httpClient.GetAsync(
            $"{_baseUrl}/api/v1/platform/why/{symbol}");
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadAsStringAsync();
    }
}

// Usage - that's it!
var client = new OptionsPlayClient("YOUR_API_KEY");
var data = await client.GetWhyAsync("AAPL");
📉 Code Reduction

Notice how the new approach eliminates ~50-70% of your authentication code. No token class, no expiry tracking, no refresh logic—just a simple header.

Developer Portal

With the new subscription key approach, you gain access to the OptionsPlay Developer Portal for self-service management and analytics.

📊

OptionsPlay Developer Portal

View usage reports, manage your subscription, and explore API documentation.

developer.optionsplay.com →

Portal Features

Feature Description
Usage Analytics View request counts, response times, and error rates over time
API Explorer Test API endpoints directly from your browser
Key Management Regenerate your subscription key if needed
Documentation Access detailed API reference and guides

Frequently Asked Questions

Do I have to migrate right away?
No. Both authentication methods are fully supported. You can migrate at your convenience or continue using the legacy OAuth approach indefinitely.
What happens to my existing clientId and clientSecret?
They continue to work. If you choose not to migrate, your existing credentials and integration will function exactly as before.
Are the API endpoints the same?
Yes, the API paths are identical. Only the base URL changes (from app.optionsplay.com to apis.optionsplay.com) and the authentication header.
Does my subscription key expire?
No. Unlike OAuth tokens, your subscription key does not expire. If you believe your key has been compromised, contact support@optionsplay.com immediately and we’ll issue a new key and revoke the old one. Self-service key regeneration via the Developer Portal is planned for a future release.
Can I use both methods during migration?
Yes. You can run both approaches in parallel—for example, migrating one service at a time while others continue using the legacy method.
Is the new approach as secure?

Yes. The subscription key approach uses the same OAuth authentication behind the scenes—the API gateway handles token management securely on your behalf. All traffic is encrypted via HTTPS (TLS 1.2+).

Isn’t one key less secure than two? What if someone steals the subscription key?

This is the most common question we get, and it’s worth answering in detail. The short version: two credentials were never “two factors.” The legacy OAuth2 client_credentials flow sends clientId and clientSecret together, in the same request, from the same machine, to obtain a bearer token. An attacker who can steal one can almost always steal the other—they live in the same config file, secrets vault entry, or environment variable block. Functionally, it’s one secret split into two fields, not defense-in-depth. And once the token is issued, every subsequent API call is authenticated with a single bearer token anyway, so the steady state of the legacy flow was already single-credential auth.

What the industry actually does

Single-credential machine-to-machine authentication is the dominant pattern among modern API vendors. A few well-known examples:

  • Stripe — single secret key (sk_live_…) in the Authorization header
  • OpenAI & Anthropic — single API key via Authorization: Bearer …
  • SendGrid, Twilio, Mailgun — single API key
  • GitHub — Personal Access Tokens and fine-grained tokens, single credential
  • Slack — bot tokens and user tokens, single credential
  • Linear, Notion, Figma, Airtable, Shopify — single API key or token
  • AWS — access key ID + secret are paired as one logical credential (you can’t use one without the other, and they’re issued and revoked as a unit)

OAuth2 client_credentials exists primarily because OAuth2 is a framework designed for delegated user authorization (the authorization_code flow, consent screens, scopes per user, etc.), and client_credentials reuses that same machinery for machine-to-machine scenarios where there is no user. The two-field shape is a historical artifact of that spec heritage, not a security property. Most API vendors who don’t need delegated user consent have moved to direct API keys because the extra round-trip, token caching, and expiry handling add operational complexity without adding meaningful security.

The controls that actually protect you (and are identical or better in the new approach)

If the concern is “what if someone steals the key?”, the answer is the same regardless of whether you have one credential or two. What matters is the surrounding controls:

  1. Transport security. All traffic is TLS 1.2+. The key is never transmitted in plaintext. This is identical for both approaches.
  2. Storage hygiene. Store the key in a secrets manager (Azure Key Vault, AWS Secrets Manager, HashiCorp Vault, GCP Secret Manager, 1Password Secrets Automation, Doppler, etc.). Never commit it to source control. Never embed it in client-side code, mobile apps, or browser JavaScript. These practices were already required for clientSecret—nothing changes.
  3. Revocation and reissuance. If you suspect a key has been compromised, contact support@optionsplay.com and we’ll revoke the existing key and issue a new one. Self-service key rotation through the Developer Portal is on our roadmap for a future APIM release. In the meantime, support-initiated rotation is a fast, human-in-the-loop process.
  4. Scoping and rate limits. APIM subscription keys are scoped to specific products and APIs, and rate-limited per key. A compromised key has a blast radius defined by its subscription—not by your entire tenant.
  5. IP allowlisting. Available via APIM policy for clients who want belt-and-braces network-level restrictions in addition to the key. Contact support if you’d like this enabled for your subscription.
  6. Observability. Every API call is logged with the subscription key identity. Anomalous usage patterns (volume spikes, unexpected geographies, off-hours traffic) are detectable, and the key can be revoked instantly.
  7. Containment without an outstanding-token window. With the legacy client_credentials flow, revoking a compromised clientSecret does not invalidate bearer tokens that have already been issued. Those outstanding tokens remain valid until they expire (up to ~60 minutes in our implementation), giving an attacker a guaranteed window of continued access after you’ve detected the breach. With a subscription key, once support revokes the key at the gateway it is invalidated immediately—there is no outstanding-token window to wait out. From the moment a breach is reported to the moment the key is dead, you’re bounded only by how fast you can reach support, not by a token TTL.

Reduced attack surface

The new approach also eliminates an entire class of bugs and attack vectors associated with token management: token caching bugs that leak credentials into logs, race conditions during token refresh, incorrect clock skew handling, token replay in misconfigured caches, and the token endpoint itself as a target for credential-stuffing attempts. Fewer moving parts means fewer things that can go wrong.

If you need stronger authentication

For clients with specific compliance drivers (PCI-DSS, regulated trading environments, SOC 2 controls that mandate certificate-based auth, etc.), the industry-standard escalation path is mutual TLS (mTLS) layered on top of the subscription key. mTLS uses a client certificate pinned to your organization, providing cryptographic proof of caller identity at the transport layer—a materially stronger control than any shared-secret scheme, whether one key or two. Azure APIM supports this natively. If you have a compliance requirement that calls for this, contact support@optionsplay.com and we’ll work with you to enable it on your subscription.

Summary

You’re not trading two secrets for one. You’re trading a two-step credential-exchange dance for direct key auth. The legacy flow’s steady state was already single-credential (the bearer token). The new approach eliminates a round-trip, removes a class of token-caching bugs, closes the outstanding-token window that exists with OAuth bearer tokens, and aligns with how nearly every modern API vendor authenticates machine-to-machine traffic. The security controls that actually matter—TLS, storage, scoping, rate limits, revocation, and monitoring—are identical or measurably better.

How do I get my subscription key?
Contact your OptionsPlay account representative or email support@optionsplay.com. We'll provision your key and provide Developer Portal access.

Support

Need help with your integration? We're here to assist.

Channel Contact
Email Support support@optionsplay.com
Developer Portal developer.optionsplay.com
API Reference Swagger Documentation