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.
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
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
- 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 ...
- 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
handled automatically
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
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");
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
app.optionsplay.com to apis.optionsplay.com)
and the authentication header.
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+).
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 theAuthorizationheader - 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:
- Transport security. All traffic is TLS 1.2+. The key is never transmitted in plaintext. This is identical for both approaches.
-
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. - 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.
- 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.
- 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.
- 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.
-
Containment without an outstanding-token window. With the legacy
client_credentials flow, revoking a compromised
clientSecretdoes 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.
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 |