OAuth2 sign-in
Integrate MarvinMC Hosting accounts using the Authorization Code flow with Discord-style identify and email scopes.
Create applicationMigrating a Discord-style login
Register a Hosting application, then replace the authorization, token and user endpoints and use its Hosting client ID and secret. Discord credentials do not work here. Register the same callback URL in the Developer Portal. The identify/email code flow and user response follow Discord conventions; bot, guild and other Discord APIs are not emulated. IDs identify Hosting accounts, not Discord accounts.
Endpoints
GEThttps://hosting.marvinmc.net/oauth2/authorizePOSThttps://hosting.marvinmc.net/api/oauth2/tokenGEThttps://hosting.marvinmc.net/api/users/@mePOSThttps://hosting.marvinmc.net/api/oauth2/token/revoke
The API endpoints also accept /api/v6 through /api/v10, for example /api/v10/users/@me. Existing /oauth/authorize, /oauth/token, /oauth/userinfo and /oauth/revoke URLs remain available. The legacy userinfo response retains sub and email_verified.
Authorization server metadataScopes
identify- Identify your account by its user ID and username.
email- Read your email address and whether it is verified.
connections.read- Read the ID and username of your linked Discord account. No Discord tokens are shared.
customer.read- Read whether your account is a paid customer. No amounts or payments are shared.
server.read- Read the selected server, its resources and live statistics.
server.start- Start the selected server.
server.stop- Stop the selected server.
server.restart- Restart the selected server.
identify is required and is the default when scope is omitted. Add email only when needed. The previous profile scope remains an alias of identify.
Server authorisation and hosting API
Enable the required scopes in the Developer Portal and include them in scope, for example identify server.read server.restart. The owner explicitly chooses one eligible server on the consent screen. Each token and its refresh tokens remain bound to that installation. Authorise again for another server and keep its tokens separately. Server scopes always require interactive consent; prompt=none returns consent_required.
For server scopes, the token response also contains server_id, the ID explicitly selected during consent. Store it with that token. Refresh preserves this binding. Identity-only responses remain unchanged.
Bearer tokens work on the customer API endpoints below. The server type must allow API access. Captcha requirements cannot be bypassed. Under Server > Permissions, owners can disable individual actions or remove the app. A new authorisation cannot revive tokens from a removed installation. Account-wide revocation also removes all server installations. Password changes, client changes and secret rotation invalidate tokens. These scopes do not permit purchases, file access, console commands or killing processes.
GET /api/client/v1/connections- connections.read
GET /api/client/v1/customer- customer.read
GET /api/client/v1/servers- server.read
GET /api/client/v1/servers/{id}- server.read
GET /api/client/v1/servers/{id}/stats- server.read
POST /api/client/v1/servers/{id}/power- server.start | server.stop | server.restart
GET /api/client/v1/jobs/{id}- Same app, server installation and action permission
curl https://hosting.marvinmc.net/api/client/v1/servers/SERVER_ID/power \
-H 'Authorization: Bearer ACCESS_TOKEN' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: UNIQUE_REQUEST_ID' \
--data '{"action":"restart"}'Power requests return HTTP 202 with a job; poll its job URL using a valid token for the same app and server installation with the action scope. Queued actions recheck the original token and current permissions before execution. Expired or revoked tokens cannot execute jobs. GET servers returns only the selected authorised server; stats returns live status and resource usage. connections returns only public Discord identity; customer returns paid_customer as a boolean. Errors: 401 invalid token, 403 insufficient permissions, 409 busy, 429 rate limit (60 requests per minute per app and user). Respect Retry-After. Responses are not cacheable.
1. Register your application
Create an application in the Developer Portal. Under General information you can add its name, icon, description and public links. Under OAuth2, register the exact callback URL and enable email access only if needed. Store the client secret exclusively on your backend, never in browser or mobile code. If you lose it, choose Regenerate secret under OAuth2: the application and client ID stay the same, and the new secret is displayed once. Replace the old secret in your integration; previous codes and tokens become invalid. Native apps use the public type without a secret. The URL generator can prepare a PKCE test request.
2. Redirect the user
Redirect to the authorization endpoint with response_type=code, client_id, the registered redirect_uri and scope=identify email. State is optional and returned unchanged if supplied (up to 2048 bytes, no control characters). A fresh random state bound to the application session is strongly recommended for CSRF protection. PKCE S256 is optional for backends with a client secret and required for public clients. The example below includes both protections: store state and verifier in your session; the verifier has 43–128 characters and the challenge is base64url(SHA-256(verifier)) without padding.
$state = bin2hex(random_bytes(32));
$verifier = rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '=');
$challenge = rtrim(strtr(base64_encode(hash('sha256', $verifier, true)), '+/', '-_'), '=');
$_SESSION['marvinmc_oauth'] = ['state' => $state, 'verifier' => $verifier];
$url = 'https://hosting.marvinmc.net/oauth2/authorize?' . http_build_query([
'response_type' => 'code',
'client_id' => 'YOUR_CLIENT_ID',
'redirect_uri' => 'https://example.com/auth/marvinmc/callback',
'scope' => 'identify email',
'state' => $state,
'code_challenge' => $challenge,
'code_challenge_method' => 'S256',
], '', '&', PHP_QUERY_RFC3986);
header('Location: ' . $url);
exit;
3. Validate the callback
If you sent state, compare the returned value with your session value in constant time and reject a missing or mismatched value. Validate the returned issuer iss against https://hosting.marvinmc.net. Your integration must protect its callback against CSRF even when omitting state; the Hosting consent form does not replace that protection. Handle error=access_denied without signing in. Exchange the code once, within 5 minutes, with the exact original redirect_uri. Send code_verifier only if a challenge was used.
4. Exchange the code
POST application/x-www-form-urlencoded, not JSON. Backend clients authenticate with HTTP Basic (URL-encode ID and secret before Base64 encoding) or client_id and client_secret in the body, never both. Public clients send client_id without a secret. Omit code_verifier when no PKCE challenge was sent. The response contains access_token, token_type=Bearer, expires_in, refresh_token and scope. Access tokens currently last 900 seconds; use expires_in rather than assuming Discord token lifetimes.
curl https://hosting.marvinmc.net/api/oauth2/token \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'client_id=YOUR_CLIENT_ID' \
--data-urlencode 'client_secret=YOUR_CLIENT_SECRET' \
--data-urlencode 'code=CALLBACK_CODE' \
--data-urlencode 'redirect_uri=https://example.com/auth/marvinmc/callback' \
--data-urlencode 'code_verifier=SESSION_VERIFIER'
5. Retrieve the user profile
Call /api/users/@me with Authorization: Bearer. Use id as the stable Hosting account key. identify returns id, username, discriminator="0", global_name and avatar=null. email and verified are included only with the email scope. A linked Discord identity is available separately with connections.read. Hosting actions require separate scopes and explicit server authorisation; spending balance is not supported.
curl https://hosting.marvinmc.net/api/users/@me \
-H 'Authorization: Bearer ACCESS_TOKEN'
{
"id": "123456789012345678",
"username": "example",
"discriminator": "0",
"global_name": "example",
"avatar": null,
"email": "example@example.com",
"verified": true
}
6. Refresh tokens
Send grant_type=refresh_token, refresh_token and the same client authentication to the token endpoint. Each refresh returns a new access token and refresh token; store both atomically. The previous tokens stop working. Refresh tokens expire 30 days after issuance. Refresh requests for one token must be serialized: reusing an old refresh token revokes its token family and requires a new login. An optional scope can reduce permissions but cannot add them. No PKCE verifier is needed for refresh requests.
curl https://hosting.marvinmc.net/api/oauth2/token \
--data-urlencode 'grant_type=refresh_token' \
--data-urlencode 'client_id=YOUR_CLIENT_ID' \
--data-urlencode 'client_secret=YOUR_CLIENT_SECRET' \
--data-urlencode 'refresh_token=REFRESH_TOKEN'
Revocation and errors
POST token (access or refresh token) and client authentication to /api/oauth2/token/revoke. This revokes the user authorization for that application, including its codes, access and refresh tokens. Unknown or already revoked tokens also return HTTP 200. Password changes, forced password changes, client edits, pausing and secret replacement invalidate affected tokens. Reusing a code revokes its token family. Handle invalid_request, invalid_client, invalid_grant, invalid_scope, invalid_token and HTTP 429; respect Retry-After.
curl https://hosting.marvinmc.net/api/oauth2/token/revoke \
--data-urlencode 'client_id=YOUR_CLIENT_ID' \
--data-urlencode 'client_secret=YOUR_CLIENT_SECRET' \
--data-urlencode 'token=ACCESS_OR_REFRESH_TOKEN' Protocol and limits
OAuth2 only: no OpenID Connect, ID tokens, password, implicit or client-credentials grants. Token, profile and revocation requests use a backend or native client; browser CORS is not enabled. Callbacks must match exactly. PKCE, when used, must be S256; public clients cannot omit it. Normal requests and prompt=consent show consent. prompt=none only succeeds for a logged-in, verified user with an unchanged, previously approved application and permissions; otherwise the callback receives login_required or consent_required. Normal passkey/2FA protections remain in place.