OAuth

SlingLab is an OAuth 2.1 authorization server. A player signs in, approves the scopes you asked for, and your app receives an access token for that player. Create a confidential or public client in the console. Public clients, including MCP apps, can also register themselves.

Discovery:

GET https://slinglab.gg/.well-known/oauth-authorization-server
Authorize https://slinglab.gg/slinglab/api/oauth/authorize
Token https://slinglab.gg/slinglab/api/oauth/token
Revoke https://slinglab.gg/slinglab/api/oauth/revoke
Register https://slinglab.gg/slinglab/api/oauth/register

What a client may do

  • Authorization code grant, with PKCE S256. Send code_challenge and code_challenge_method=S256. A plain challenge is rejected.
  • Refresh tokens. Each refresh replaces the previous refresh token. Sending an old refresh token revokes that token family.
  • state on the authorize request. SlingLab returns it with the code.
  • A redirect URL that matches one you registered, character for character. https is allowed. http is allowed only for localhost. A native app can use a reverse-DNS scheme such as com.example.app:/callback. Fragments, wildcards, and embedded usernames are rejected.
  • An optional resource (RFC 8707). If you send it on authorize, send the same value on the token request. Use https://slinglab.gg/slinglab/api for Your decks and Analytics, or https://slinglab.gg/mcp for MCP.

Access tokens last 1 hour. Refresh tokens last 30 days. SlingLab does not offer the implicit grant, the password grant, or client credentials.

A confidential client sends its secret with HTTP Basic (client_id and client_secret) or as client_secret_post. A public client sends client_id and no secret. The token and revoke endpoints accept application/x-www-form-urlencoded only.

The token endpoint allows 30 requests a minute for each client. Revoke allows 30 a minute for each client. Registration allows 20 attempts an hour from one network address. Over the limit, the response is HTTP 429 with error rate_limited and a Retry-After header.

Dynamic registration (POST JSON to the register endpoint) creates a public client and returns client_id. It does not return a secret.

Scopes

Ask for the scopes you need, separated by spaces.

Scope What the player is sharing
decks:read Every deck on their account, including private decks and decklists. See Your decks.
analytics:read The same career and deck analytics as their Analytics page. See Analytics.
games:read Finished games. Used by the MCP game history tool.
cards:read Card lookup in the SlingLab catalog.
rules:read The Magic Comprehensive Rules.
mcp Opens the MCP endpoint. Each tool still needs its own scope.

The player sees these on the consent screen. If they have already approved a set of scopes that covers the new request, SlingLab skips the screen. They can disconnect an app from the console.

Example

A confidential client asks for a player's decks. The response bodies are in Your decks and Analytics.

Send the player to authorize

Open this URL in the player's browser. code_challenge is the base64url SHA-256 of a code_verifier you keep on your server. This pair is the sample from RFC 7636: verifier dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk.

GET /slinglab/api/oauth/authorize?response_type=code&client_id=sl_app_your_client&redirect_uri=https%3A%2F%2Fexample.com%2Fcallback&scope=decks%3Aread&state=xyz&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256&resource=https%3A%2F%2Fslinglab.gg%2Fslinglab%2Fapi HTTP/1.1
Host: slinglab.gg

The player signs in and approves the scopes. SlingLab redirects to the URL you registered, with code and the same state:

https://example.com/callback?code=sl_ac_the_code&state=xyz

Exchange the code

POST /slinglab/api/oauth/token HTTP/1.1
Host: slinglab.gg
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(client_id:client_secret)

grant_type=authorization_code&code=sl_ac_the_code&redirect_uri=https%3A%2F%2Fexample.com%2Fcallback&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk&resource=https%3A%2F%2Fslinglab.gg%2Fslinglab%2Fapi

A public client sends client_id in the body and no secret. The token response:

{
  "access_token": "sl_at_the_token",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "sl_rt_the_refresh",
  "scope": "decks:read"
}

Call the API

GET /slinglab/api/v1/me/decks HTTP/1.1
Host: slinglab.gg
Authorization: Bearer sl_at_the_token

That body is Your decks. Add analytics:read to the authorize scope and the same pattern calls Analytics. MCP uses the token the same way, with resource https://slinglab.gg/mcp and the mcp scope.

Refresh

POST /slinglab/api/oauth/token HTTP/1.1
Host: slinglab.gg
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(client_id:client_secret)

grant_type=refresh_token&refresh_token=sl_rt_the_refresh

The response is a new access token and a new refresh token. Store the new refresh token. The previous refresh token stops working, and sending it again revokes that token family.

Errors

Token and register errors use OAuth fields:

{ "error": "invalid_grant", "error_description": "Authorization code is invalid or expired." }

Revoke returns 200 and {} when the client authenticated, including when the token is already unknown.