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 Redirect the user to the authorization endpoint.
- 2 Exchange the returned code for an access token.
- 3 Call the userinfo endpoint with that token to read the user's profile.
client_id and client_secret, and register your redirect URIs.Endpoints
| Purpose | Method | URL |
|---|---|---|
| Authorize | GET | https://nhg2za62llvcqknchzv5qcpc.lata.ethans.app/api/oauth/authorize |
| Token | POST | https://nhg2za62llvcqknchzv5qcpc.lata.ethans.app/api/oauth/token |
| User info | GET | https://nhg2za62llvcqknchzv5qcpc.lata.ethans.app/api/oauth/userinfo |
| Account events | POST | https://nhg2za62llvcqknchzv5qcpc.lata.ethans.app/api/oauth/events |
Scopes
Request only what you need. Space-separate multiple scopes in the scope parameter.
| Scope | Grants access to |
|---|---|
profile | Name, username, avatar, bio, account creation date |
email | Email address and email-verified status |
social | Linked website, GitHub, Twitter, and Discord |
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..."
}| Field | Type | Description |
|---|---|---|
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_verified—trueonce 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,truewhenrolesincludesSPARKCLOUD(orADMIN).
Requiring verification
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_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.
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"
}'| Field | Required | Description |
|---|---|---|
type | Yes | Event type — see table below |
sub | Yes | Spark Account user ID (UUID) of the affected user |
reason | No | Human-readable reason string (used with user.suspended) |
client_id | If no Basic auth | Your app's client ID |
client_secret | If no Basic auth | Your app's client secret |
Event types
| Type | Description |
|---|---|
user.suspended | The user's account was suspended. Includes an optional reason string. |
user.unsuspended | A previously suspended account has been reinstated. |
user.deleted | The 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
}| Field | Type | Description |
|---|---|---|
type | string | Event type (user.suspended, user.unsuspended, user.deleted) |
sub | string | Spark Account user ID (UUID) of the affected user |
reason | string | null | Suspension reason, if provided |
ts | number | Unix 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 });
});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.
| Error | Meaning |
|---|---|
invalid_request | Missing or malformed required parameters |
invalid_client | Client ID / secret did not match |
invalid_grant | Code expired, already used, or mismatched redirect URI |
access_denied | The user declined to authorize your app |
unsupported_grant_type | grant_type must be authorization_code or refresh_token |
invalid_event | Unknown event type or missing sub field in events endpoint |
invalid_json | Request body could not be parsed as JSON |