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://sparkden.org/api/oauth/authorize |
| Token | POST | https://sparkden.org/api/oauth/token |
| User info | GET | https://sparkden.org/api/oauth/userinfo |
| JWKS (id_token keys) | GET | https://sparkden.org/api/oauth/jwks |
| Send email | POST | https://sparkden.org/api/oauth/email |
| Account events | POST | https://sparkden.org/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 |
email.send | Send branded emails to the user through Spark Account — verified apps only |
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://sparkden.org/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",
"id_token": "eyJhbGci..." // OIDC identity token (RS256, verify via JWKS)
}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://sparkden.org/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,
"chat_member": true,
"chat_admin": false,
"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, CHAT_MEMBER, CHAT_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 |
chat_member | boolean | True if the user is a full member of the community chat (SparkChat) |
chat_admin | boolean | True if the user is a community chat admin |
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,CHAT_MEMBER,CHAT_ADMIN,ADMIN. Gate access on the role you need (e.g.SPARKCLOUD).sparkcloud_access— convenience boolean,truewhenrolesincludesSPARKCLOUD(orADMIN).
Requiring verification
Sending email
Verified apps can send emails to their users through Spark Account — no SMTP setup, no separate email provider. Every message is wrapped in a clean, Spark-styled template that carries your app's branding and a clear "sent from [your app] via Spark Account" disclosure, so users always know who's emailing them and why.
email.send scope during authorization. Unverified apps receive app_not_verified.Branding
Set a logo URL and brand color on your app under Account → OAuth Apps. The logo appears at the top of every email (and on the consent screen); the brand color is used for the accent bar and call-to-action button. No branding set? Emails fall back to a neutral monogram and Spark orange.
Request
Authenticate with the user's access token. The email is always delivered to the token holder's account email address — you never need (or get) the address itself.
curl -X POST https://sparkden.org/api/oauth/email \
-H "Authorization: Bearer ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"subject": "Your weekly project report",
"title": "Your projects this week",
"body": "Hi! You shipped 4 new builds this week.\n\nKeep it up — your streak is now 3 weeks.",
"cta_url": "https://yourapp.com/dashboard",
"cta_text": "View dashboard"
}'| Field | Required | Description |
|---|---|---|
subject | Yes | Email subject line (max 150 characters) |
body | Yes | Plain-text message (max 5,000 characters). Use blank lines for paragraphs — HTML is not allowed and will be escaped. |
title | No | Heading shown at the top of the email (defaults to the subject, max 150 characters) |
cta_url | No | http(s) link for an optional call-to-action button |
cta_text | No | Button label (defaults to "Open [app name]", max 60 characters) |
Response
{ "ok": true }Content rules & limits
- Plain text only. Any HTML in
subject,title, orbodyis escaped, not rendered. Blank lines become paragraph breaks. - One link. The optional
cta_urlbutton is the only clickable link — it must be a valid http(s) URL. - Rate limits. Per user, each app may send 5 emails/hour and 20 emails/day. Exceeding either returns
rate_limited(HTTP 429). - Revocable. Users can stop your emails at any time by revoking your app's access from their account.
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://sparkden.org/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://sparkden.org/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 |
insufficient_scope | The access token is missing the scope required for this endpoint (e.g. email.send) |
app_not_verified | Email sending requires a verified app — submit your app for review first |
rate_limited | Email quota reached for this user (5/hour, 20/day per app) — HTTP 429 |
email_unavailable | Email delivery is temporarily unavailable — retry later (HTTP 503) |