Developer Docs

Spark Account OAuth API

Let your application authenticate users with their Spark Account and read verified profile data using the standard OAuth 2.0 authorization-code flow.

Overview

Spark Account implements the OAuth 2.0 authorization code grant. The flow is three steps:

  1. 1 Redirect the user to the authorization endpoint.
  2. 2 Exchange the returned code for an access token.
  3. 3 Call the userinfo endpoint with that token to read the user's profile.
Get your credentials. Create an app under Account → OAuth Apps to get a client_id and client_secret, and register your redirect URIs.

Endpoints

PurposeMethodURL
AuthorizeGEThttps://nhg2za62llvcqknchzv5qcpc.lata.ethans.app/api/oauth/authorize
TokenPOSThttps://nhg2za62llvcqknchzv5qcpc.lata.ethans.app/api/oauth/token
User infoGEThttps://nhg2za62llvcqknchzv5qcpc.lata.ethans.app/api/oauth/userinfo
Account eventsPOSThttps://nhg2za62llvcqknchzv5qcpc.lata.ethans.app/api/oauth/events

Scopes

Request only what you need. Space-separate multiple scopes in the scope parameter.

ScopeGrants access to
profileName, username, avatar, bio, account creation date
emailEmail address and email-verified status
socialLinked website, GitHub, Twitter, and Discord

1. Authorization request

Send the user's browser to the authorization endpoint. After they approve, they're redirected back to your redirect_uri with a code (and your state).

https://nhg2za62llvcqknchzv5qcpc.lata.ethans.app/api/oauth/authorize?
  response_type=code
  &client_id=YOUR_CLIENT_ID
  &redirect_uri=https://yourapp.com/callback
  &scope=profile email
  &state=RANDOM_STRING
ParameterRequiredDescription
response_typeYesMust be code
client_idYesYour app's client ID
redirect_uriYesMust exactly match a registered URI
scopeNoSpace-separated scopes (defaults to profile)
stateRecommendedOpaque value echoed back for CSRF protection

2. Token exchange

Exchange the authorization code for an access token from your server. Credentials can be sent in the body (shown) or as HTTP Basic auth (Authorization: Basic base64(client_id:client_secret)).

curl -X POST https://nhg2za62llvcqknchzv5qcpc.lata.ethans.app/api/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "code=THE_CODE_FROM_REDIRECT" \
  -d "redirect_uri=https://yourapp.com/callback" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET"

Response

{
  "access_token": "eyJhbGci...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "eyJhbGci...",
  "scope": "profile email"
}

3. User info

Use the access token to fetch the user's profile. Fields returned depend on the granted scopes — email and email_verified are only included when the email scope was granted.

curl https://nhg2za62llvcqknchzv5qcpc.lata.ethans.app/api/oauth/userinfo \
  -H "Authorization: Bearer ACCESS_TOKEN"

Response

{
  "sub": "a1b2c3d4-...",
  "uid": "a1b2c3d4-...",
  "username": "janedoe",
  "email": "jane@example.com",      // only with email scope
  "email_verified": true,            // only with email scope
  "roles": ["ADULT", "VERIFIED", "SPARKCLOUD"],
  "role": "VERIFIED",
  "verified": true,
  "id_verified": true,
  "sparkcloud_access": true,
  "name": "Jane Doe",
  "avatar_url": "https://...",
  "created_at": "2026-01-12T..."
}
FieldTypeDescription
sub / uid string Stable unique user ID (UUID)
username string Account username
email string Email address — only returned when email scope is granted
email_verified boolean Whether the email address is confirmed — only returned when email scope is granted
roles string[] All roles, e.g. STUDENT, ADULT, VERIFIED, SPARKCLOUD, MATTERMOST_MEMBER, MATTERMOST_ADMIN, ADMIN
role string Legacy single role (STUDENT, ADULT, VERIFIED, MODERATOR, or ADMIN)
verified boolean True if the user has passed identity verification
id_verified boolean Alias of verified — identity verification status
sparkcloud_access boolean True if the user may access SparkCloud
name string Display name (requires profile scope)
avatar_url string Profile picture URL (requires profile scope)

ID verification

Spark Account can verify a user's real-world identity. Your app can both read a user's verification status and require it before authorization.

Reading verification status

The userinfo response includes the user's verification state — check whichever you prefer:

  • verified / id_verifiedtrue once the user has passed identity verification.
  • roles — the full set, e.g. STUDENT/ADULT (account type), VERIFIED, SPARKCLOUD, MATTERMOST_MEMBER, MATTERMOST_ADMIN, ADMIN. Gate access on the role you need (e.g. SPARKCLOUD).
  • sparkcloud_access — convenience boolean, true when roles includes SPARKCLOUD (or ADMIN).

Requiring verification

Enable "Require ID verification" on your app (in the create or edit dialog under OAuth Apps) to block unverified users at the consent screen. They'll be prompted to verify their identity before they can authorize your app — so any token your app receives belongs to a verified user.

Refresh tokens

Access tokens expire after 1 hour. Use the refresh token to get a new pair without sending the user through the flow again. Refresh tokens are single-use — store the new one from each response.

Client credentials required. The refresh token grant requires your client_id and client_secret — either in the request body or as HTTP Basic auth. Requests without valid credentials are rejected with invalid_client.
curl -X POST https://nhg2za62llvcqknchzv5qcpc.lata.ethans.app/api/oauth/token \
  -d "grant_type=refresh_token" \
  -d "refresh_token=YOUR_REFRESH_TOKEN" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET"

Account events

The events endpoint lets a registered app notify Spark Account of account lifecycle changes — for example, when your app suspends or deletes a user. Spark Account applies the change locally and re-fans it to every other connected app.

Back-channel only. This endpoint is for server-to-server use. Authenticate with your client_id and client_secret via HTTP Basic auth or request body. Never expose your client secret in frontend code.

Request

curl -X POST https://nhg2za62llvcqknchzv5qcpc.lata.ethans.app/api/oauth/events \
  -H "Content-Type: application/json" \
  -H "Authorization: Basic $(echo -n 'CLIENT_ID:CLIENT_SECRET' | base64)" \
  -d '{
    "type": "user.suspended",
    "sub": "a1b2c3d4-...",
    "reason": "Violated community guidelines"
  }'
FieldRequiredDescription
typeYesEvent type — see table below
subYesSpark Account user ID (UUID) of the affected user
reasonNoHuman-readable reason string (used with user.suspended)
client_idIf no Basic authYour app's client ID
client_secretIf no Basic authYour app's client secret

Event types

TypeDescription
user.suspendedThe user's account was suspended. Includes an optional reason string.
user.unsuspendedA previously suspended account has been reinstated.
user.deletedThe user's account was permanently deleted. You should remove their data.

Response

{ "ok": true }

Outbound webhooks

Register an Event Webhook URL on your OAuth app (via an admin) and Spark Account will POST account lifecycle events to it whenever a user is suspended, unsuspended, or deleted — whether triggered by an admin action or by another connected app.

Payload

// POST to your registered eventWebhookUrl
// Headers:
//   Content-Type: application/json
//   X-Spark-Event: user.suspended
//   X-Spark-Signature: sha256=abc123...

{
  "type": "user.suspended",
  "sub": "a1b2c3d4-...",
  "reason": "Violated community guidelines",
  "ts": 1748908800000
}
FieldTypeDescription
typestringEvent type (user.suspended, user.unsuspended, user.deleted)
substringSpark Account user ID (UUID) of the affected user
reasonstring | nullSuspension reason, if provided
tsnumberUnix timestamp in milliseconds

Verifying signatures

Every webhook request includes an X-Spark-Signature header of the form sha256=<hex>. Compute an HMAC-SHA256 of the raw request body using your client secret as the key and compare using a constant-time function. Reject requests where the signature doesn't match.

import crypto from 'crypto';

function verifySparkSignature(rawBody, signature, clientSecret) {
  const expected = 'sha256=' +
    crypto.createHmac('sha256', clientSecret)
          .update(rawBody)
          .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature)
  );
}

// In your webhook handler:
app.post('/webhook', express.text({ type: '*/*' }), (req, res) => {
  const sig = req.headers['x-spark-signature'];
  if (!verifySparkSignature(req.body, sig, CLIENT_SECRET)) {
    return res.status(401).json({ error: 'invalid_signature' });
  }
  const event = JSON.parse(req.body);
  // handle event.type: 'user.suspended' | 'user.unsuspended' | 'user.deleted'
  res.json({ ok: true });
});
Respond quickly. Spark Account does not retry failed webhook deliveries. Return a 2xx status as soon as the signature is verified — do any heavy processing asynchronously.

Errors

Token and userinfo errors are returned as JSON with an error field. Authorization errors are appended to your redirect_uri as query parameters.

ErrorMeaning
invalid_requestMissing or malformed required parameters
invalid_clientClient ID / secret did not match
invalid_grantCode expired, already used, or mismatched redirect URI
access_deniedThe user declined to authorize your app
unsupported_grant_typegrant_type must be authorization_code or refresh_token
invalid_eventUnknown event type or missing sub field in events endpoint
invalid_jsonRequest body could not be parsed as JSON