Snapie Auth API
A purpose-built Hive account gateway and signing proxy. Authenticates users via Google or email, creates custodial Hive accounts, and signs blockchain operations on their behalf.
Overview
Snapie Auth bridges Web2 identity (Google, email) to Hive blockchain accounts. It handles the full lifecycle: account creation using pre-claimed tokens (ACTs), custodial key management, Hive operation signing, and voluntary key export (emancipation).
External services integrate via the Internal API using a shared Bearer token for account provisioning and sponsor token issuance.
Auth & Cookies
Sessions are stored in two cookies set by the server after any successful login:
| Cookie | Purpose | Flags |
|---|---|---|
snapieauth_session | JWT RS256 session token | httpOnly, Secure, SameSite=Lax |
snapieauth_csrf | CSRF double-submit token (JS-readable) | Secure, SameSite=Lax |
All state-mutating requests must include the CSRF token as the x-csrf-token header. Always send credentials: 'include' in fetch calls. There is no Bearer token for user-facing APIs.
Custody Modes
Custodial — Snapie generates all four Hive keypairs (owner, active, posting, memo) server-side, derived from a single master password via Hive's fromLogin. The master password and keys are AES-256-GCM encrypted using a server-derived key. The user never sees them until voluntary emancipation, when both the master password and all four keys are returned. All signing — posting and active — is handled transparently.
Emancipated — User holds their own keys. Snapie only has posting authority (granted by the user). Posting-level ops are still proxied. Active-level ops return an unsigned operation for the client to sign via Aioha or Keychain:
{ "needsClientSigning": true, "unsignedOp": ["op_name", {...}], "account": "alice", "keyType": "active" }
The account and keyType fields are always present so the consuming app can call Aioha or Keychain directly without additional context.
Error Codes
All errors return HTTP 4xx/5xx with body { "error": "error_code_string" }.
| Code | Status | Meaning |
|---|---|---|
| unauthorized | 401 | No valid session cookie |
| admin_required | 403 | Endpoint requires isAdmin |
| csrf_invalid | 403 | Missing or wrong x-csrf-token header |
| already_linked | 409 | User already has a Hive account |
| previously_had_account | 403 | User already used their free account (sponsor token overrides) |
| global_daily_limit | 429 | Daily free account quota exhausted — resets midnight UTC |
| ip_daily_limit | 429 | Per-IP daily quota exhausted |
| no_acts | 503 | Snapie has no pre-claimed account tokens |
| invalid_username | 400 | Bad format or username already taken on-chain |
| insufficient_rc | 503 | Insufficient Resource Credits. For posting ops: the Snapie service account is out of RC (operator issue). For active ops: the user's own account is low. |
| emancipation_required | 403 | Account value exceeded threshold; user must export keys first |
| account_too_new | 403 | Custodial account hasn't reached the minimum age to emancipate yet (EMANCIPATION_MIN_AGE_DAYS, default 30). Response includes eligibleAt. |
| account_disabled | 403 | Account was disabled by an admin (abuse review). Login still works; all transacting is blocked. Response includes reason if one was given. |
| not_custodial | 400 | Action requires custodial mode |
| invalid_or_expired_challenge | 400 | Link challenge not found or expired (TTL 5 min) |
| invalid_signature | 401 | Hive posting key signature did not verify |
| posting_authority_required | 403 | @snapie does not have posting authority on the account |
| email_not_verified | 403 | Email address has not been verified yet |
| user_not_found | 404 | No account with that email (admin: grant admin) |
| cannot_demote_self | 400 | Admin tried to revoke their own access |
| op_not_allowed | 403 | Operation type is not permitted via the broadcast endpoint |
Asset Format
All monetary amounts are strings, never plain numbers.
| Currency | Format | Example |
|---|---|---|
| HIVE / HBD | 3 decimal places | "1.000 HIVE", "0.500 HBD" |
| VESTS | 6 decimal places | "1.000000 VESTS" |
POST /auth/google
Sign in or register with a Google ID token. Creates a user record on first login. Sets session and CSRF cookies.
Request
| Field | Type | Description |
|---|---|---|
| credential | string | Google ID token from the OAuth popup / One Tap response |
Response — { user: SnapieUser }
{
"user": {
"id": "6a272a4eac383450fbb0bc30",
"name": "Alice",
"picture": "https://lh3.googleusercontent.com/...",
"hiveUsername": null,
"custodyMode": null,
"isAdmin": false,
"disabled": false
}
}
hiveUsername is null until the user creates or links a Hive account.
POST /auth/apple
Sign in or register with an Apple identity token. Creates a user record on first login. Sets session and CSRF cookies.
Apple quirk: iOS only provides the user's name and email on the very first sign-in. On all subsequent logins Apple returns neither — the server preserves whatever was saved. Your mobile app must pass name on the first login and omit it (or send null) on subsequent ones.
Request
| Field | Type | Description |
|---|---|---|
| identityToken | string | JWT from ASAuthorizationAppleIDCredential.identityToken |
| name | object|null | Optional. { firstName, lastName } — only sent on first sign-in. Ignored if both fields are absent. |
Response — { user: SnapieUser }
{
"user": {
"id": "6a272a4eac383450fbb0bc30",
"name": "Alice Doe",
"picture": null,
"hiveUsername": null,
"custodyMode": null,
"isAdmin": false,
"disabled": false
}
}
hiveUsername is null until the user creates or links a Hive account.
Environment variable
Set APPLE_CLIENT_ID to your app's Bundle ID (e.g. io.snapie.app). The server validates the aud claim in the identity token against this value.
React Native example
import { appleAuth } from '@invertase/react-native-apple-authentication'
const appleAuthRequestResponse = await appleAuth.performRequest({
requestedOperation: appleAuth.Operation.LOGIN,
requestedScopes: [appleAuth.Scope.EMAIL, appleAuth.Scope.FULL_NAME],
})
const { identityToken, fullName } = appleAuthRequestResponse
// fullName.givenName / familyName are only populated on first sign-in
const name = fullName?.givenName
? { firstName: fullName.givenName, lastName: fullName.familyName ?? undefined }
: null
const res = await fetch('/api/auth/apple', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ identityToken, name }),
})
const { user } = await res.json()
Email / Password
Register a new email account. Sends a verification email. Returns 202 { pending: true } — no session is created until the email is verified.
| Field | Type | Description |
|---|---|---|
| name | string | Display name |
| string | Email address — stored as HMAC-SHA256 hash only | |
| password | string | Min 8 chars — hashed with Argon2id |
Log in with email and password. Returns 403 email_not_verified if verification is pending. Sets session and CSRF cookies on success.
| Field | Type |
|---|---|
| string | |
| password | string |
Response: { user: SnapieUser }
Resend the verification email. Rate limited to 5 req/hr per IP.
| Field | Type |
|---|---|
| string |
Response: { ok: true }
Email verification link handler. Verifies the token, activates the account, and redirects to /manage.html. This is called by the link in the verification email — not typically called directly.
GET /auth/me
Returns the current session user. For custodial accounts with a Hive username, also returns live account value and emancipation status.
{
"user": {
"id": "6a272a4eac383450fbb0bc30",
"name": "Alice",
"picture": "https://lh3.googleusercontent.com/...",
"hiveUsername": "alice",
"custodyMode": "custodial",
"isAdmin": false,
"disabled": false
},
"email": "alice@example.com",
"accountValueUsd": 3.21,
"emancipationRequired": false
}
email, accountValueUsd, and emancipationRequired are only present when applicable.
POST /auth/logout
Clears both session cookies. No request body. Returns { ok: true }.
GET /account/eligibility
Check if the current user can create a free Hive account.
{
"canCreate": true,
"canLinkExisting": true,
"sponsored": false,
"reason": null
}
sponsored: true means a sponsor token was found for this email — quota limits and the "previously had account" restriction are bypassed. reason is set when canCreate is false: already_linked, previously_had_account, no_acts, ip_daily_limit, global_daily_limit.
GET /account/check-username/:username
Check if a Hive username is available on-chain. Rate limited to 40 req/min.
{ "available": true }
{ "available": false, "reason": "already_taken" }
POST /account/create
Queue a Hive account creation job. Rate limited to 5 req/hr.
| Field | Type | Description |
|---|---|---|
| username | string | Desired Hive username (3–16 chars) |
| custodyMode | string | "custodial" or "emancipated" |
| ownerPub | string? | STM… public key. Required if emancipated. |
| activePub | string? | Required if emancipated. |
| postingPub | string? | Required if emancipated. |
| memoPub | string? | Required if emancipated. |
For custodial mode, all keypairs are generated server-side — no pubkeys needed.
{ "jobId": "abc123", "sponsored": false }
Poll GET /account/job/:jobId to track progress.
GET /account/job/:jobId
Poll job status. Recommended interval: every 3–5 seconds.
Status progression: pending → broadcasting → acked → confirmed (or failed / expired).
{
"jobId": "abc123",
"username": "alice",
"status": "confirmed",
"txId": "abc123def456...",
"error": null
}
On confirmed, the user's hiveUsername is set. Call GET /auth/me to refresh the session.
GET /account/value
Live account balances and USD value. Requires a linked Hive account.
{
"hiveBalance": "4.200 HIVE",
"hbdBalance": "1.500 HBD",
"vestingShares": "5000.000000 VESTS",
"hiveValueUsd": 1.26,
"hbdValueUsd": 1.50,
"vestingValueUsd": 2.10,
"totalValueUsd": 4.86,
"emancipationRequired": false
}
POST /link/verify-challenge
Request a challenge nonce to sign with your Hive posting key. The challenge expires after 5 minutes.
| Field | Type | Description |
|---|---|---|
| hiveUsername | string | The Hive account to link |
{ "challenge": "snapie-auth:link:1717700000:a3f9...", "snapieAccount": "snapie" }
snapieAccount is the Hive account that needs posting authority — use it with requestAddAccountAuthority in Keychain before confirming.
POST /link/confirm
Verify the signed challenge and link the Hive account. @snapie must already have posting authority on the account before this is called.
| Field | Type | Description |
|---|---|---|
| username | string | The Hive account to link |
| challenge | string | The challenge string from verify-challenge |
| signature | string | Hex-encoded signature from Hive posting key |
{ "ok": true, "hiveUsername": "alice" }
Errors: invalid_or_expired_challenge, invalid_signature, posting_authority_required, already_linked.
POST /hive/sign-message
Sign an arbitrary message with the user's Hive posting key. Custodial: server signs silently. Emancipated: returns the message for client signing. 30 req/min.
| Field | Type | Description |
|---|---|---|
| message | string | The message to sign. Max 512 chars. |
// Custodial
{ "signature": "1f3a...", "account": "alice" }
// Emancipated
{ "needsClientSigning": true, "message": "...", "account": "alice", "keyType": "posting" }
Signature is SHA-256 + secp256k1 (compatible with dhive/hive-js verifySignature).
Emancipated client signing:
// Keychain window.hive_keychain.requestSignBuffer(account, message, 'Posting', cb) // Aioha aioha.signMessage(message, KeyTypes.Posting)
POST /hive/broadcast
Sign and broadcast a posting-level Hive operation using Snapie's posting authority. Works for all users (custodial and emancipated). 60 req/min.
Allowed op types: vote, comment, delete_comment, custom_json, claim_reward_balance.
| Field | Type | Description |
|---|---|---|
| op | array | A 2-element array: ["op_type", { ...fields }] |
// Vote
{ "op": ["vote", { "voter": "alice", "author": "bob", "permlink": "my-post", "weight": 10000 }] }
// Root post
{ "op": ["comment", {
"parent_author": "", "parent_permlink": "hive",
"author": "alice", "permlink": "my-slug",
"title": "My Post", "body": "Content here.",
"json_metadata": "{\"tags\":[\"hive\"],\"app\":\"myapp/1.0\"}"
}] }
// Custom JSON
{ "op": ["custom_json", {
"required_auths": [],
"required_posting_auths": ["alice"],
"id": "myapp",
"json": "{\"action\":\"follow\",\"target\":\"bob\"}"
}] }
Response: { "txId": "abc123..." }
Note: json_metadata and json must be JSON-encoded strings, not objects.
POST /hive/transfer
Transfer HIVE or HBD. Active-level op. 10 req/min.
| Field | Type | Description |
|---|---|---|
| to | string | Recipient Hive username (3–16 chars) |
| amount | string | "N.NNN HIVE" or "N.NNN HBD" |
| memo | string? | Optional. Max 2048 chars. |
// Custodial
{ "txId": "abc123..." }
// Emancipated
{ "needsClientSigning": true, "unsignedOp": ["transfer", {...}], "account": "alice", "keyType": "active" }
Savings
Move HIVE or HBD into savings. Active-level op. No lockup on deposit; savings earn interest. 10 req/min.
| Field | Type | Description |
|---|---|---|
| amount | string | "N.NNN HIVE" or "N.NNN HBD" |
| to | string? | Recipient — defaults to self |
| memo | string? | Optional. Max 2048 chars. |
Initiate a savings withdrawal. Funds arrive after 3 days. Active-level op. 10 req/min.
| Field | Type | Description |
|---|---|---|
| amount | string | "N.NNN HIVE" or "N.NNN HBD" |
| to | string? | Recipient — defaults to self |
| memo | string? | Optional. Max 2048 chars. |
| requestId | integer? | Must be unique per account. Auto-generated from Math.floor(Date.now()/1000) if omitted. |
Power Up / Down
Stake HIVE as Hive Power. Active-level op. 10 req/min.
| Field | Type |
|---|---|
| amount | string — "N.NNN HIVE" |
Begin 13-week HIVE unstaking. Active-level op. 10 req/min.
| Field | Type | Description |
|---|---|---|
| amount | string | "N.NNNNNN VESTS" — 6 decimal places. To convert HP → VESTS: hp × total_vesting_shares / total_vesting_fund_hive from getDynamicGlobalProperties(). Set to "0.000000 VESTS" to cancel an active power-down. |
POST /hive/delegate
Delegate Hive Power. Active-level op. 10 req/min.
| Field | Type | Description |
|---|---|---|
| delegatee | string | Recipient Hive username |
| amount | string | "N.NNNNNN VESTS" — set to "0.000000 VESTS" to remove delegation |
Convert
Convert HBD → HIVE via the debt conversion mechanism (~3.5 day wait, settles at median price). Active-level op. 10 req/min.
| Field | Type | Description |
|---|---|---|
| amount | string | "N.NNN HBD" only |
| requestId | integer? | Auto-generated if omitted |
Convert HIVE → HBD instantly using collateralized conversion. HBD is delivered immediately; excess collateral HIVE is returned after ~3.5 days. Active-level op. 10 req/min.
| Field | Type | Description |
|---|---|---|
| amount | string | "N.NNN HIVE" only |
| requestId | integer? | Auto-generated if omitted |
Limit Orders
Place a resting limit order or instant market swap on the internal HIVE/HBD market. Active-level op. 10 req/min.
| Field | Type | Description |
|---|---|---|
| sell | string | "N.NNN HIVE" or "N.NNN HBD" — what you give up |
| receive | string | Opposite currency to sell — minimum you'll accept |
| fillOrKill | boolean? | false (default) = resting limit order. true = instant market swap — order cancels immediately if not fully fillable. |
| expiresInSeconds | integer? | Default 3600, max 2592000 (30 days). Ignored when fillOrKill is true. |
| orderId | integer? | uint32, unique per account. Auto-generated from timestamp if omitted. |
Note: a fillOrKill=true order that cannot fill still returns { txId } — the transaction was broadcast but no swap occurred. Verify balances or check open orders to confirm a fill.
Cancel an open limit order. Active-level op. 10 req/min.
| Field | Type | Description |
|---|---|---|
| orderId | integer | Must match an open order owned by the authenticated user |
POST /hive/claim-rewards
Claim all pending reward balances (HIVE, HBD, VESTS). Posting-level op — proxied for all users including emancipated. No request body needed; amounts are read from the chain automatically.
{ "txId": "abc123..." }
GET /payment/fee
Returns the current Hive account creation fee, derived live from the top-20 witness median via get_chain_properties. Cached server-side for 10 minutes — not hardcoded.
{ "amountHive": "3.000 HIVE", "amountUsd": 1.20 }
amountUsd is calculated from the live HIVE price feed.
POST /payment/hive-intent
Create a HIVE payment intent for account creation. The server generates a unique UUID memo. The user must send exactly the stated HIVE amount to the receiving account with that memo in the transfer. The server polls the Hive blockchain every 5 seconds and confirms the intent on detection. Rate limited to 10 req/hr.
No request body required.
{
"memo": "a1b2c3d4-e5f6-...",
"receivingAccount": "snapie",
"amount": "3.000 HIVE",
"amountUsd": 1.20,
"expiresAt": "2026-06-12T16:30:00.000Z"
}
Send the transfer via Keychain or Aioha with memo as the transfer memo. Poll GET /payment/intent/:memo to track confirmation. Intent expires after 30 minutes.
POST /payment/lightning-intent
Create a Bitcoin Lightning payment intent via v4v.app. v4v converts sats to HIVE and forwards the payment to the operator's Hive account with the memo included. Rate limited to 10 req/hr.
No request body required.
{
"memo": "a1b2c3d4-e5f6-...",
"invoice": "lnbc1234...",
"amountUsd": 1.20,
"expiresAt": "2026-06-12T16:30:00.000Z"
}
Display invoice as a QR code for the user to scan with any Lightning wallet. Poll GET /payment/intent/:memo to track confirmation. Invoice expires after 30 minutes.
GET /payment/intent/:memo
Poll the status of a payment intent. Recommended interval: every 3–5 seconds.
{
"memo": "a1b2c3d4-e5f6-...",
"type": "hive",
"status": "confirmed",
"txId": "abc123def456...",
"amountHive": "3.000 HIVE",
"amountUsd": 1.20,
"expiresAt": "2026-06-12T16:30:00.000Z"
}
Status values: pending, confirmed, expired. On confirmed, the user is eligible to create a Hive account via POST /account/create — the paid flag bypasses the daily quota check.
Returns 404 not_found if the memo doesn't exist or belongs to a different user.
GET /login
Entry point for plugin license activation. A plugin's activation page redirects the user here with redirect, plugin, and machine query params. The user signs in with Google; Snapie gets-or-creates their custodial Hive account, broadcasts the activation op, and redirects back to redirect with hive_user and status appended. No further API calls are needed by the activation page.
https://auth.snapie.io/login ?redirect=https://licensing.menosoft.xyz/activate &plugin=pechi_saturator &machine=a3f9b2c1d4e5f6a7
redirect must point at one of the hostnames in LICENSE_REDIRECT_HOSTS (default licensing.menosoft.xyz) or the page refuses to proceed. machine must be 16 hex characters.
POST /license/activate
Internal endpoint used by the /login flow after Google sign-in. Reuses the user's existing custodial Hive account if they have one, otherwise auto-generates a username and creates one. Broadcasts a pechi_license custom_json activate op from the account.
Request
{ "plugin": "pechi_saturator", "machine": "a3f9b2c1d4e5f6a7" }
Response
{ "status": "activated", "hiveUser": "josemena-a1b2c3" }
On failure: { "status": "error", "error": "no_acts" | "timeout" | "insufficient_rc" | ... }. Always responds 200 — the caller appends the result to the redirect URL either way.
GET /emancipate/status
{
"custodyMode": "custodial",
"hiveUsername": "alice",
"accountValueUsd": 3.40,
"threshold": 10,
"forcedEmancipation": false,
"minAgeDays": 30,
"eligibleAt": "2026-08-15T00:00:00.000Z",
"ageGateMet": false
}
forcedEmancipation: true is informational only — it does not currently block any action, it's shown as a UI nudge to export keys.
ageGateMet: false means the account hasn't reached the minimum age (minAgeDays, from EMANCIPATION_MIN_AGE_DAYS) to emancipate yet — an abuse-prevention hold on newly created custodial accounts. POST /emancipate/start will reject with 403 account_too_new until eligibleAt.
POST /emancipate/start
Decrypt and return all four private keys, plus the master password they derive from. Immediately wipes the encrypted copies from the server. Custodial accounts only. 5 req/hr. This action is irreversible.
Before returning keys, verifies them against on-chain public keys to guarantee correctness.
masterPassword is the single secret all four keys derive from (importable into any Hive wallet to regenerate them). It is null for accounts created before master-password support.
Fails with 403 account_too_new (see status above) if the account hasn't reached the minimum hold age, or 403 account_disabled if an admin has disabled the account.
{
"keys": {
"owner": "5J...",
"active": "5J...",
"posting": "5J...",
"memo": "5J..."
},
"masterPassword": "P5J...",
"message": "Save these keys now. They will not be shown again. This server has deleted its copy."
}
POST /internal/provision-account
Create a Hive account without a user session. Used by external services to sell or gift accounts. Requires Authorization: Bearer <INTERNAL_API_KEY>.
| Field | Type | Description |
|---|---|---|
| username | string | Desired Hive username |
| custodyMode | string | "custodial" or "emancipated" |
| ownerPub… | string? | STM… public keys — required if emancipated |
| linkedEmail | string? | Associate with an existing or new user by email. Creates a placeholder user if not registered yet. |
| returnKeysOnce | boolean? | Custodial only — decrypt and return keys on the first confirmed poll, then wipe them |
| note | string? | Audit note stored with the job |
{ "jobId": "abc123", "userId": "6a272a4eac383450fbb0bc30" }
GET /internal/jobs/:jobId
Poll a provisioned job. If returnKeysOnce was set and status is confirmed, decrypts and returns keys exactly once then permanently wipes them.
{
"jobId": "abc123",
"username": "alice",
"status": "confirmed",
"custodyMode": "custodial",
"txId": "abc123def456",
"keys": { "owner": "5J...", "active": "5J...", "posting": "5J...", "memo": "5J..." },
"error": null
}
keys is only present on the first call after confirmation when returnKeysOnce was set. Subsequent calls return { ...base, "keysAlreadyDelivered": true }.
POST /internal/issue-sponsor-token
Issue a one-time sponsor token for an email address. When the user registers with that email and creates a Hive account, the token is consumed automatically — their account creation bypasses the daily quota and the "previously had account" restriction.
| Field | Type | Description |
|---|---|---|
| string | Recipient email | |
| note | string? | e.g. "earned 500 credits" |
| expiresInDays | number? | Optional TTL in days — no expiry if omitted |
{ "tokenId": "...", "email": "alice@example.com", "note": "...", "expiresAt": null }
GET /admin/stats
{ "actCount": 14, "pendingJobs": 2, "userCount": 387, "incompleteOnboarding": 5, "heldAccounts": 12 }
heldAccounts is the count of custodial accounts still inside the emancipation hold window (see Accounts below).
GET /admin/jobs
Last 100 account creation jobs across all users.
POST /admin/claim-act
Claim one pre-claimed account token (ACT) from the Snapie service account using its RC. No request body.
{ "ok": true, "txId": "abc123..." }
Sponsor Token Management
List all sponsor tokens. Pass ?includeUsed=false to hide consumed tokens.
Issue a sponsor token. Optionally sends an invite email to the recipient (even if they haven't registered yet).
| Field | Type | Description |
|---|---|---|
| string | Recipient email | |
| note | string? | Internal note |
| expiresInDays | number? | Optional TTL |
| sendEmail | boolean? | Send an invite email to the recipient |
{ "tokenId": "...", "emailSent": true }
Revoke an unused sponsor token. Returns { ok: true }.
Admin User Management
List all admin users.
{ "admins": [{ "userId": "...", "name": "Alice", "provider": "google", "createdAt": "..." }] }
Grant admin access to an existing user by email.
| Field | Type | Description |
|---|---|---|
| string | Must already be registered |
{ "userId": "...", "name": "Bob", "provider": "google" }
Revoke admin access. Cannot demote yourself. Returns { ok: true }.
Accounts (Abuse Review)
Custodial accounts hold a 30-day emancipation hold by default (EMANCIPATION_MIN_AGE_DAYS) so admins have time to review new faucet-created accounts before the user can export keys. These endpoints back the Accounts tab in /admin.html.
Search accounts by Hive username (prefix match) or display name. Case-insensitive, up to 25 results.
{ "users": [{
"userId": "6a272a4eac383450fbb0bc30",
"hiveUsername": "alice",
"name": "Alice",
"provider": "google",
"custodyMode": "custodial",
"createdAt": "2026-07-20T00:00:00.000Z",
"disabled": false,
"disabledReason": null,
"disabledAt": null
}] }
Custodial accounts still inside the emancipation hold window, newest first (up to 200) — the abuse-review queue. Same shape as /admin/users plus eligibleAt.
{ "minAgeDays": 30, "users": [{ ..., "eligibleAt": "2026-08-19T00:00:00.000Z" }] }
Disable an account for abuse. The user can still log in, but every transacting endpoint (/hive/*, /emancipate/start) rejects with 403 account_disabled.
| Field | Type | Description |
|---|---|---|
| reason | string | Required, 1–500 chars. Shown to the user. |
{ "userId": "...", "disabled": true, "disabledReason": "faucet abuse", "disabledAt": "..." }
Re-enable a previously disabled account. Clears all disabled* fields.
{ "userId": "...", "disabled": false, "disabledReason": null, "disabledAt": null }