Skip to main content

Connect Chastify with OAuth

Use this flow for a Connect Chastify button in a dashboard or Discord bot integration. Chastify authenticates its own account and returns a stable chastifyUserId. Your backend associates that ID with the Discord user already authenticated in your dashboard.

This is an OAuth authorization-code flow with S256 PKCE and explicit consent. It does not look up arbitrary Discord IDs, require a personal DEV key from your users, or change their existing Discord verification in Chastify. Identity access works even without an active lock. This is not OpenID Connect: there is no ID token.

CredentialUsed byPurpose
OAuth client IDBrowser authorization request and your backendIdentifies the registered application; it is not a secret.
App-scoped DEV keyYour backend onlyAuthenticates POST /api/apps/oauth/token. It must belong to the client ID used for authorization.
User OAuth access tokenYour backend onlyReads that consenting user's /api/apps/v1/me and, if authorized, /api/apps/v1/session.

A user-wide DEV key is for personal programs; an iframe mainToken is for installed extensions. Neither replaces an OAuth credential.

Register your application​

  1. Choose Developer API → Create OAuth app and enter your app's name and slug. OAuth apps do not require iframe runtime or setup URLs and cannot be installed as lock extensions.
  2. Enter your exact URL under Callback URL. If you need additional callbacks for development or staging, add them on separate lines. HTTPS is required, except HTTP loopback URLs such as http://localhost:3000/callback. Wildcards, URL credentials, and fragments are not supported. At least one callback is required when creating the app; removing all callbacks later disables account linking. Enable Allow lock read access if your integration needs lock information; otherwise the app has profile access only.
  3. Save the app, then open its edit form to copy the OAuth client ID (its application ID).
  4. In the Developer API key form, select Key type → App-scoped, choose your OAuth application, and select Create DEV key. Copy the full key immediately; it is shown only once. The key uses the app's allowed read access when created. Keep it on your backend.

The developer's key-creation safeguard can block creating an app-scoped key while they wear an active lock. Create the required key before starting that lock. This is separate from an end user's consent: people connecting to your integration do not create Developer keys.

If you add lock-read access later, an existing profile-only key does not gain that scope automatically. Create a key with the needed access and request fresh consent including locks:read from users whose grant was profile-only.

The authenticated management API accepts appType: "oauth", oauthRedirectUris: string[] (up to 10 entries), and defaultScopes: ["locks:read"] or [] for profile-only access in POST /api/extensions/dev/apps. Edit callbacks and scopes through PATCH /api/extensions/dev/apps/:appId; the app type cannot change after creation. Existing lock extensions can also configure OAuth callbacks in their extension form.

Start from your authenticated dashboard​

Require an authenticated session in your own dashboard before starting the flow. For a Discord integration, that session must already identify the Discord account. Generate a cryptographically random, single-use state and a PKCE verifier. Store them server-side with that session's verified Discord user ID, the client ID, callback URL, and a short expiry. Do not accept the Discord user ID from callback query parameters.

import { createHash, randomBytes } from 'node:crypto';

const clientId = process.env.CHASTIFY_CLIENT_ID;
if (!clientId) throw new Error('Configure your Chastify OAuth client ID');

const state = randomBytes(32).toString('base64url');
const verifier = randomBytes(32).toString('base64url');
const challenge = createHash('sha256').update(verifier).digest('base64url');

const authorize = new URL('https://chastify.net/apps/authorize');
authorize.search = new URLSearchParams({
response_type: 'code',
client_id: clientId,
redirect_uri: 'https://your-dashboard.example/oauth/chastify/callback',
scope: 'profile:read locks:read',
state,
code_challenge: challenge,
code_challenge_method: 'S256',
}).toString();
// Save the session-bound state and verifier before redirecting to authorize.href.

profile:read is required. Omit locks:read for identity-only linking. Open the authorization URL as a top-level browser navigation; the consent page refuses to run inside an iframe. The user signs in to Chastify and explicitly approves the displayed account, application, callback origin, and requested access. Cancellation returns error=access_denied and the original state to the registered callback.

Handle the callback on your backend​

Before exchanging a code, require the same authenticated dashboard session, validate and consume its stored state, and verify iss=https://chastify.net. Reject missing, expired, reused, or mismatched state. A changed Discord login must start a new connection flow. Use a dedicated callback route with no third-party scripts, Cache-Control: no-store, and Referrer-Policy: no-referrer. Validate the session, state, and issuer for both success and cancellation. If error=access_denied, finish without exchanging a code; reject other error responses or a missing code. After handling the callback, redirect to a clean dashboard URL so the code does not remain in the visible page URL.

Exchange the authorization code within five minutes:

curl --fail-with-body https://chastify.net/api/apps/oauth/token \
-H "Authorization: Bearer YOUR_APP_SCOPED_DEV_KEY" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'client_id=YOUR_APP_ID' \
--data-urlencode 'redirect_uri=https://your-dashboard.example/oauth/chastify/callback' \
--data-urlencode 'code=CODE_FROM_CALLBACK' \
--data-urlencode 'code_verifier=YOUR_STORED_VERIFIER'

JSON bodies are also supported. The callback URL must exactly match the registered URL used when authorizing. Codes are single-use and bound to the application, callback, user, and PKCE challenge. The token endpoint uses your existing app-scoped DEV key as a backend bearer credential; a personal key or iframe mainToken cannot replace it.

The response contains access_token, token_type: "Bearer", expires_in: 2592000, and the granted scope. Access lasts 30 days, with no refresh token in this version. Repeat the consent flow after expiry. If a successful exchange response is lost, start a new flow; the consumed code cannot be exchanged again.

Read and store the verified account mapping​

curl --fail-with-body https://chastify.net/api/apps/v1/me \
-H "Authorization: Bearer USER_OAUTH_ACCESS_TOKEN"
{
"chastifyUserId": "stable-chastify-user-id",
"username": "example",
"profileUrl": "https://chastify.net/users/example",
"avatarUrl": "https://example-cdn/avatar-url"
}

avatarUrl is nullable. There is no separate display name or profile slug in this response. Store chastifyUserId as the stable identity; usernames can change. Bind it to the Discord ID from the authenticated session that initiated the flow. Chastify verifies the Chastify side; your Discord OAuth session verifies the Discord side. Possession of an arbitrary Discord ID is not proof of ownership.

Keep access tokens encrypted at rest on your backend and out of browser code, URLs, logs, and Discord messages. Enforce your own mapping ownership/uniqueness rules before replacing an existing link. An identity mapping is not permanent authorization to read lock information: use a currently valid token for every refresh.

Read lock identities and media​

curl --fail-with-body https://chastify.net/api/apps/v1/session \
-H "Authorization: Bearer USER_OAUTH_ACCESS_TOKEN"

This requires locks:read. A personal user-wide DEV key with locks:read can also call this endpoint. An app-scoped key alone cannot read another user's lock; use the OAuth token obtained with their consent. Existing installed-extension APIs still use their app-scoped key plus x-chastify-main-token.

The response also groups module status under modules.builtIn (boolean flags) and modules.extensions (installed-extension identities and enabled flags). Disabled modules are included so dashboards can list both states. See Module status for the response shape and filtering example. /api/apps/v1/me does not include lock modules.

The response includes these nullable fields under lock:

WearerKeyholderLock
wearerUserIdownerUserIdphotoUrl
wearerUsernameownerUsername
wearerProfileUrlownerProfileUrl
wearerAvatarUrlownerAvatarUrl

These are current-session participants. A missing keyholder or unavailable image returns null. Media URLs are resolved through Chastify's storage access rules, including signed URL mode and cover availability checks. URLs may expire; refresh the API response rather than storing image URLs as permanent identifiers. These fields describe the lock's cover image, not private verification photos.

Only locks where the token owner is the wearer or current keyholder are accessible. Lock-read consent is not bound to the one lock active when consent was given: a valid grant can read other eligible current or later active sessions within its lifetime.

Without a selector, the API chooses an active candidate and that choice can change as locks are updated. To keep a dashboard on a specific lock, use its exact 24-character hexadecimal ID:

curl --fail-with-body https://chastify.net/api/apps/v1/session \
-H "Authorization: Bearer USER_OAUTH_ACCESS_TOKEN" \
-H "x-chastify-lock-id: $CHASTIFY_LOCK_ID"

Validate the configured ID and confirm that the response's lock._id matches it. Omit selectors for automatic selection. An empty, malformed, repeated array, or conflicting selector returns 400 invalid_selector; it never falls back to another lock. A valid but unavailable or unauthorized target returns 409 no_active_lock_session. An ID does not grant access to another user's lock.

The shared resolver accepts lock IDs through x-chastify-lock-id, x-lock-id, or lockId in query, route parameters, or a JSON body. Extension-session IDs use x-chastify-extension-session-id, x-extension-session-id, sessionId, or extensionSessionId. Supplied values must be 24-character hexadecimal strings. If both a lock and extension-session selector are supplied, they must identify the same lock. Validation also applies to personal Developer API write requests.

No active authorized lock returns 409 no_active_lock_session; this does not mean account linking failed. /me can still succeed. The personal-API use safeguard also blocks OAuth lock reads when the consenting account is the wearer, returning 403 personal_dev_api_key_blocked_while_locked. It does not block the profile read or turn keyholder requests into wearer requests.

Privacy​

  • infoHidden=true hides card information, not wearer/keyholder identity or media. It is not a public-sharing permission.
  • For wearer access, hidden remaining time, an unfinished blind unlock gamble, or hidden card-game information makes remainingSeconds, lock.releaseAt, lock.maxReleaseAt, lockData.timeRemainingSeconds, and lockData.maxTimeRemainingSeconds return null. Do not convert null into zero or show a countdown. Keyholder access retains its normal visibility.
  • Timing and helper flags are not a complete unlock decision. Do not infer permission to unlock from isLocked, unlockable, task points, or a zero countdown.
  • Online-status opt-outs keep the corresponding last-seen timestamps null.
  • Each participant's gender and sexual-orientation visibility settings apply to lockData, including the participant who did not connect the app. Hidden values return empty strings, and setting changes apply on the next API read with an existing token.
  • lockData.wearerProfileLocation and lockData.keyholderProfileLocation remain empty strings for compatibility. This endpoint does not return login geolocation.
  • A successful API response authorizes the connected application to read the data. Get the user's agreement before posting their information to a Discord channel; prefer private/ephemeral bot responses.

Disconnect and credential boundaries​

Users can disconnect at Developer API → Manage connected applications, or directly at Connected applications. The list groups active grants by application. Disconnecting an application invalidates all of that user's existing grants and pending authorization codes for the app on subsequent API requests. Other users and other apps are unaffected. A fresh consent flow can reconnect afterward; retrying the old disconnect does not revoke that new consent. Revocation does not erase information the integration already stored, so provide your own unlink/data-removal controls too.

Other changes affect access as follows:

ChangeEffect on existing OAuth tokens
Revoke the app-scoped key used to exchange a codeTokens exchanged with that key stop working. Merely creating a replacement key does not move existing grants to it.
Remove a callback URLExisting tokens and pending codes for that exact callback are permanently invalidated. Re-adding it requires fresh consent; other callbacks remain valid.
Narrow the app's allowed lock-read scopeLock reads stop; profile reads can continue. Access is limited by the original consent and the current app/key scopes.
Disable the app/API access, delete the app, or delete/ban the developer or consenting accountRequests fail while their required account/app is unavailable.
Reach the access token's expiryThe user must complete a fresh consent flow; there is no refresh-token flow.

Plan app-key rotation around existing connections: new exchanges can use the new key, but revoking the old one requires its connected users to reconnect. Callback removal is permanent revocation for its existing grants, even if the URL is added again. Concurrent callback edits can return 409 conflict; reload the app before trying again.

OAuth access tokens are accepted only by GET /api/apps/v1/me and GET /api/apps/v1/session. They cannot modify locks or call installed-extension actions. Neither read requires an iframe launch or mainToken.

Response or symptomRecovery
Authorization page says the request is invalidCheck the exact client ID/callback, active app, requested scopes, state, and S256 PKCE parameters. Return to your app to start again.
Callback error=access_deniedValidate state/issuer, then show a cancelled connection. Do not exchange a code.
Token endpoint 400 invalid_requestCheck the required fields, authorization-code grant type, and PKCE verifier format.
Token endpoint 400 invalid_grantCheck the app-scoped key belongs to this client and permits requested lock reads; also check exact callback/verifier and whether the five-minute code expired or was consumed. Start a fresh flow after correcting the cause.
Read endpoint 401 invalid_tokenCheck expiry, revocation, app key, callback registration, and account/app availability. Reconnect once the cause is resolved.
Session 403 insufficient_scopeThe grant, app, and exchange key must all allow locks:read. New consent alone cannot repair a key or app missing that permission.
Session 403 personal_dev_api_key_blocked_while_lockedRespect the wearer's safeguard; reconnecting does not bypass it. Profile linking can remain valid.
Session 400 invalid_selectorCorrect or omit the explicit selector; empty, malformed, and conflicting values are rejected before lock selection.
Session 409 no_active_lock_sessionKeep the verified identity mapping and show that no eligible lock is available. Check the selected ID.
Consent reports a connection limitChastify limits currently valid, unexpired grants to 50 per user, including short-lived pending codes. Disconnect unused connections before trying again.
429Back off according to the response. Avoid polling or repeated connection attempts in a tight loop.

Check HTTP status before consuming profile/session fields, and distinguish linked identity from current lock-read availability in your dashboard. Do not repeatedly retry rejected credentials or reuse a consumed authorization code.

The first-party consent and connection-management APIs require a normal Chastify login and its CSRF protections. They are used by the Chastify UI, not by an integration backend impersonating the user.

Protocol references: OAuth security recommendations and S256 PKCE.