Developer Documentation
Everything you need to integrate BalancYZ β client management, JWT auth, Google OAuth, webhooks, and plan management.
https://api.balancyz.com
Authentication
BalancYZ supports two authentication methods depending on how your clients are registered. All protected endpoints require an API token; JWT-protected endpoints also require a client JWT.
Method 1: client_id_only
Traditional API key auth. Clients created via POST /clients. Access via API token only.
Method 2: email_password
Email/password with JWT. Clients registered via POST /auth/register. Requires API token + JWT.
X-API-Key Header (Preferred)
Recommended for all server-to-server calls.
Authorization Bearer
Alternative format β same key, different header.
JWT Token Authentication (email_password clients)
Expiration
24 hours
Claims
user_id, client_id, email
Signing
HS256 + LARAVEL_APP_KEY
Required for: GET /auth/client, POST /auth/debit, POST /auth/logout β these require both API token AND JWT.
β οΈ Security: Never embed your API token in client-side code, Figma plugins, or browser extensions. Always proxy calls through a backend server that holds the token securely.
Health Check
No Auth Required/health
Check API health status. Use this to confirm connectivity before running integration tests.
Response 200
{
"status": "ok"
}
Client Management
These endpoints use client_id_only authentication. Clients are end-users of your app. The client_id can be any unique string: a Figma user ID, hashed email, UUID, etc.
/clients
Auth Required
Register a new client. Idempotent β safe to call on every session open. Returns 201 for new, 409 for existing. Both are success states.
Request Body
{
"name": "John Doe",
"client_id": "unique_client_id",
"metadata": "{\"custom\":\"data\"}"
}
namestring, requiredclient_idstring, required β unique ID for this usermetadataJSON string, optionalResponse 201 Created
{
"name": "John Doe",
"client_id": "unique_client_id",
"balance": 0,
"currency": "USD",
"created_at": "2026-01-06T12:00:00Z",
"updated_at": "2026-01-06T12:00:00Z"
}
400 Bad Request
Validation error
401 Unauthorized
Invalid or missing API token
409 Conflict
client_id already exists β safe to ignore
/clients/{'{client_id}'}
Auth Required
Retrieve a client's balance, plan info, and status. Also returns plan object if a plan is assigned.
Response 200 β With Plan
{
"name": "John Doe",
"client_id": "unique_client_id",
"balance": 1000,
"currency": "USD",
"status": "active",
"balance_expired_at": "2026-12-31T23:59:59Z",
"client_plan_key": "premium_7_rgmm6f",
"plan_expired_at": "2026-02-15T16:13:29Z",
"plan": {
"plan_key": "premium_7_rgmm6f",
"name": "Premium Plan",
"price": 29.99,
"plan_expired_at": "2026-02-15T16:13:29Z"
},
"created_at": "2026-01-06T12:00:00Z",
"updated_at": "2026-01-06T12:05:00Z"
}
Response 200 β No Plan
{
"name": "John Doe",
"client_id": "unique_client_id",
"balance": 1000,
"currency": "USD",
"status": "active",
"created_at": "2026-01-06T12:00:00Z",
"updated_at": "2026-01-06T12:05:00Z"
}
Response 404
{
"error": "client_not_found",
"message": "Client not found"
}
Balance & Debit
Debit tokens from a client after a premium action. Always debit after the action completes. A 402 means zero balance β show your upsell prompt.
/clients/{'{client_id}'}/debit
Auth Required
Deduct tokens from a client's balance. Returns full client object on success.
Request Body
{
"amount": 100
}
Response 200 β
{
"name": "John Doe",
"client_id": "unique_client_id",
"balance": 900,
"currency": "USD",
"status": "active",
"created_at": "2026-01-06T12:00:00Z",
"updated_at": "2026-01-06T12:05:00Z"
}
Response 402 β
{
"error": "insufficient_balance"
}
Client Data
Store and retrieve arbitrary metadata for any client. Works for all clients regardless of registration method. This is an admin-only endpoint accessed via API token.
/clients/{'{client_id}'}/data
Auth Required
Retrieve all stored metadata for a client.
Response 200
{
"metadata": {
"custom": "data",
"key": "value"
}
}
/clients/{'{client_id}'}/data
Auth Required
Update client metadata. Completely replaces existing metadata β not merged.
β οΈ This replaces all existing metadata. Send the full metadata object each time.
Request Body
{
"metadata": {
"custom": "updated_data",
"new_key": "new_value",
"nested": {
"object": true
}
}
}
Response 200
{
"metadata": {
"custom": "updated_data",
"new_key": "new_value",
"nested": { "object": true }
}
}
Email / Password Authentication
These endpoints use the email_password method. Clients register with email + password and receive a JWT token for subsequent authenticated requests.
/auth/register
API Token Required
Register a new client with email and password. Returns JWT immediately if email verification is disabled.
Content-Type: application/json
Origin: https://balancyz.com
Request Body
{
"email": "user@balancyz.com",
"password": "secure_password123",
"name": "John Doe",
"metadata": {
"custom": "data"
}
}
Response 201 β Verification Disabled
{
"token": "eyJhbGciOiJIUzI1NiJ9...",
"client": {
"name": "John Doe",
"client_id": "user@balancyz.com",
"balance": 0,
"currency": "USD",
"status": "active",
"created_at": "2026-01-06T12:00:00Z"
},
"expires_at": "2026-01-07T12:00:00Z"
}
Response 201 β Email Verification Enabled
{
"message": "Registration successful. Please check your email to verify your account.",
"email_sent": true,
"status": "pending"
}
/auth/login
API Token Required
Authenticate with email and password. Returns a JWT token valid for 24 hours.
Request Body
{
"email": "user@balancyz.com",
"password": "secure_password123"
}
Response 200
{
"token": "eyJhbGciOiJIUzI1NiJ9...",
"client": {
"name": "John Doe",
"client_id": "user@balancyz.com",
"balance": 1000,
"currency": "USD",
"status": "active",
"created_at": "2026-01-06T12:00:00Z"
},
"expires_at": "2026-01-07T12:00:00Z"
}
/auth/client
API Token + JWT Required
Get the authenticated client's full profile including plan info. Requires both API token and JWT token.
X-API-Key: YOUR_API_TOKEN
Response shape is the same as GET /clients/{'{client_id}'} β includes plan object if a plan is assigned.
/auth/debit
API Token + JWT Required
Debit balance for the JWT-authenticated client. The client is resolved from the JWT β no need to pass client_id.
Request Body
{
"amount": 50
}
Response 200 / 402
// 200 β success
{ "balance": 950 }
// 402 β insufficient
{ "error": "insufficient_balance" }
/auth/logout
API Token + JWT Required
Log out the authenticated client by deleting the device session associated with their JWT token. Only affects the current device β other sessions remain valid.
X-API-Key: YOUR_API_TOKEN
Request Body
Response 200
{
"message": "Logged out successfully"
}
How it works
- Extracts user and client info from the JWT
- Checks authentication method is email_password
- Extracts the device ID from the request
- Deletes the device session from the database
- Other active sessions for the same client remain valid
Forgot Password
A secure, time-limited, single-use token system for password recovery via email. Works only with email_password authentication when email verification is enabled.
π Token Security
256-bit cryptographically secure tokens, SHA-256 hashed in DB β plain token never stored
β±οΈ Expiry & Limits
30-minute expiration, single-use tracked with used_at, rate limited to 3 requests per 15 min
π‘οΈ Anti-Enumeration
Same success response for all emails β prevents user enumeration attacks
/auth/forgot-password
API Token Required
Request a password reset email. Always returns success to prevent user enumeration. Rate limited to 3 requests per 15 minutes per email.
Request Body
{
"email": "user@example.com"
}
Response 200
{
"success": true,
"message": "If this email is registered,
you will receive a reset link shortly."
}
/auth/reset-password/verify?token=xxx
Optional
Verify if a reset token is valid before showing the reset form. Useful for UI validation.
Response 200 β Valid
{
"valid": true,
"message": "Token is valid",
"expires_at": "2026-01-19T15:30:00Z"
}
Response 400 β Invalid/Expired
{
"valid": false,
"message": "Invalid or expired token"
}
/auth/reset-password
API Token Required
Submit the reset token and new password. Token is single-use and expires after 30 minutes.
Request Body
{
"token": "abcdef1234567890...",
"new_password": "NewSecurePassword123!"
}
Response 200
{
"success": true,
"message": "Password has been reset successfully.
You can now login with your new password."
}
# 1. Request reset email
curl -X POST https://api.balancyz.com/auth/forgot-password \
-H "X-API-Key: your_api_token" \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com"}'
# 2. User receives email:
# https://yourapp.com/reset-password?token=TOKEN_HERE
# 3. (Optional) Verify token before showing form
curl -X GET "https://api.balancyz.com/auth/reset-password/verify?token=TOKEN_HERE"
# 4. Submit new password
curl -X POST https://api.balancyz.com/auth/reset-password \
-H "X-API-Key: your_api_token" \
-H "Content-Type: application/json" \
-d '{"token":"TOKEN_HERE","new_password":"NewPass123!"}'
# 5. Login with new password
curl -X POST https://api.balancyz.com/auth/login \
-H "X-API-Key: your_api_token" \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","password":"NewPass123!"}'
Configuration & Prerequisites
User Settings
Configure at Settings
forgot_password_enabledmust be trueforgot_password_redirect_urlyour frontend reset page URLEnvironment Variables
BREVO_API_KEY=your_brevo_api_key BREVO_SENDER_EMAIL=noreply@yourdomain.com BREVO_SENDER_NAME=Your App Name
Google OAuth
Allow clients to sign in with their Google account. The flow uses an encrypted API key in the URL and exchanges an auth_code for a JWT token. Requires google_oauth_enabled in user settings and GOOGLE_CLIENT_ID + GOOGLE_CLIENT_SECRET in your environment.
OAuth Flow Overview
/auth/google/{key}
to Google
+ auth_code
/auth/login/auth_code
returned β
/auth/google/{'{encrypted_api_key}'}
No Auth
Initiate the Google OAuth flow. Decrypts the API key, validates it, and redirects the user to Google's OAuth page.
How It Works
- Decrypts API key using
API_KEY_PASSKEYorLARAVEL_APP_KEY - Validates API key in database
- Checks
google_oauth_enabledin user settings - Creates encrypted state (user_id + created_at)
- Redirects to Google OAuth page
Error Responses
State expires after 15 minutes.
/auth/google/callback
No Auth
Handles the Google OAuth callback. Exchanges the code for user info, creates or updates the client, and redirects with an auth_code.
Query Parameters
codeAuthorization code from GooglestateEncrypted state (user_id + created_at)Redirect URL Format
How It Works
- Receives code and state from Google
- Decrypts & validates state (15-min expiry)
- Exchanges code for Google access token
- Gets user email from Google
- Creates or updates client with random password
- Generates auth_code (encrypted email + datetime)
- Redirects with auth_code in query string
β οΈ auth_code expires after 15 seconds β exchange it immediately.
/auth/login/auth_code
API Token Required
Exchange the short-lived auth_code from the OAuth redirect for a full JWT token. Must be called within 15 seconds of receiving the auth_code.
Content-Type: application/json
Request Body
{
"auth_code": "encrypted_auth_code_here"
}
Response 200
{
"token": "eyJhbGciOiJIUzI1NiJ9...",
"client": {
"name": "John Doe",
"client_id": "user@gmail.com",
"balance": 0,
"currency": "USD",
"status": "active",
"created_at": "2026-01-06T12:00:00Z"
},
"expires_at": "2026-01-07T12:00:00Z"
}
// After Google redirects to your frontend:
// https://yourapp.com/callback?auth_code=eyJpdiI6...
const urlParams = new URLSearchParams(window.location.search);
const authCode = urlParams.get('auth_code');
// β‘ Must be done within 15 seconds!
const data = await fetch('https://api.balancyz.com/auth/login/auth_code', {
method: 'POST',
headers: {
'X-API-Key': 'YOUR_API_TOKEN',
'Content-Type': 'application/json',
},
body: JSON.stringify({ auth_code: authCode })
}).then(r => r.json());
// Store JWT and use for future requests
localStorage.setItem('jwt_token', data.token);
Plan Management
Create and manage custom subscription plans for your clients. Plans can be monthly (with configurable duration) or lifetime. Assign a default plan in settings and it will be automatically applied to all newly registered clients.
π Monthly Plans
Duration of 1β120 months. plan_expired_at auto-calculated from duration_months.
βΎοΈ Lifetime Plans
Never expire. plan_expired_at is null. duration_months is null.
Plan Key Format
Plan keys are auto-generated as: {'{name_slug}'}_{'{user_id}'}_{'{6_random_chars}'}
Plan Name: "Premium Plan" β plan_key: "premium_plan_7_a3b5c7" Plan Name: "Free" β plan_key: "free_7_nv5i4j"
/my-plans
Auth Required
List all plans created by the authenticated user (account owner).
Response 200
[
{
"id": 2,
"user_id": 7,
"plan_key": "premium_7_rgmm6f",
"name": "Premium Plan",
"description": "Premium plan with all features",
"price": 29.99,
"duration_months": 1,
"created_at": "2026-01-06T12:00:00Z",
"updated_at": "2026-01-06T12:00:00Z"
}
]
Error Responses
| HTTP Code | Error Key |
|---|---|
| 401 | unauthorized |
| 402 | insufficient_balance |
| 404 | client_not_found |
| 409 | client_id_exists |
| 422 | validation_error |
| 500 | server_error |
Examples
// 1. Register user on every plugin open (idempotent)
const userId = figma.currentUser.id;
await fetch('https://your-backend.com/api/register', {
method: 'POST',
body: JSON.stringify({ name: figma.currentUser.name, client_id: userId })
}); // 201 β
or 409 β
// 2. Check balance
const { balance } = await fetch(`https://your-backend.com/api/balance/${userId}`).then(r => r.json());
if (balance < 10) { figma.notify('β οΈ Buy tokens!'); return; }
// 3. Debit after premium action
await fetch(`https://your-backend.com/api/debit/${userId}`, {
method: 'POST', body: JSON.stringify({ amount: 10 })
}); // 200 { balance: 490 } β
402 insufficient β
β οΈ Security: Never embed your API token in plugin code. Always proxy through a backend server.