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://sparkden.org/api/oauth/authorize
TokenPOSThttps://sparkden.org/api/oauth/token
User infoGEThttps://sparkden.org/api/oauth/userinfo
JWKS (id_token keys)GEThttps://sparkden.org/api/oauth/jwks
Send emailPOSThttps://sparkden.org/api/oauth/email
Account eventsPOSThttps://sparkden.org/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
email.sendSend branded emails to the user through Spark Account — verified apps only

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://sparkden.org/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://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..."
}
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, 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_verifiedtrue once 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, 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.

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.

Verified apps only. Your app must be approved by an admin (submit it for review) and the user must have granted the 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"
  }'
FieldRequiredDescription
subjectYesEmail subject line (max 150 characters)
bodyYesPlain-text message (max 5,000 characters). Use blank lines for paragraphs — HTML is not allowed and will be escaped.
titleNoHeading shown at the top of the email (defaults to the subject, max 150 characters)
cta_urlNohttp(s) link for an optional call-to-action button
cta_textNoButton label (defaults to "Open [app name]", max 60 characters)

Response

{ "ok": true }

Content rules & limits

  • Plain text only. Any HTML in subject, title, or body is escaped, not rendered. Blank lines become paragraph breaks.
  • One link. The optional cta_url button 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.
Use it for transactional mail — receipts, alerts, reports, security notices. Marketing blasts and spam will get your app's verified status revoked.

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://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.

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://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"
  }'
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
insufficient_scopeThe access token is missing the scope required for this endpoint (e.g. email.send)
app_not_verifiedEmail sending requires a verified app — submit your app for review first
rate_limitedEmail quota reached for this user (5/hour, 20/day per app) — HTTP 429
email_unavailableEmail delivery is temporarily unavailable — retry later (HTTP 503)