API Reference v1

Developer Documentation

Everything you need to integrate BalancYZ β€” client management, JWT auth, Google OAuth, webhooks, and plan management.

Base URL 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)

X-API-Key: your_api_token_here

Recommended for all server-to-server calls.

Authorization Bearer

Authorization: Bearer your_api_token_here

Alternative format β€” same key, different header.

JWT Token Authentication (email_password clients)

Authorization: Bearer your_jwt_token_here

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

POST /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, required
client_idstring, required β€” unique ID for this user
metadataJSON string, optional

Response 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

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

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

GET /clients/{'{client_id}'}/data Auth Required

Retrieve all stored metadata for a client.

Response 200

{
  "metadata": {
    "custom": "data",
    "key": "value"
  }
}
POST /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.

POST /auth/register API Token Required

Register a new client with email and password. Returns JWT immediately if email verification is disabled.

Authorization: Bearer YOUR_API_TOKEN
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"
}
POST /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"
}
GET /auth/client API Token + JWT Required

Get the authenticated client's full profile including plan info. Requires both API token and JWT token.

Authorization: Bearer YOUR_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.

POST /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" }
POST /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.

Authorization: Bearer YOUR_JWT_TOKEN
X-API-Key: YOUR_API_TOKEN

Request Body

// No body required

Response 200

{
  "message": "Logged out successfully"
}

How it works

  1. Extracts user and client info from the JWT
  2. Checks authentication method is email_password
  3. Extracts the device ID from the request
  4. Deletes the device session from the database
  5. 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

POST /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."
}
GET /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"
}
POST /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."
}
Complete Forgot Password Flow β€” cURL
# 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 true
forgot_password_redirect_urlyour frontend reset page URL

Environment 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

User visits
/auth/google/{key}
β†’
Redirected
to Google
β†’
Callback
+ auth_code
β†’
Exchange
/auth/login/auth_code
β†’
JWT token
returned βœ…
GET /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

  1. Decrypts API key using API_KEY_PASSKEY or LARAVEL_APP_KEY
  2. Validates API key in database
  3. Checks google_oauth_enabled in user settings
  4. Creates encrypted state (user_id + created_at)
  5. Redirects to Google OAuth page

Error Responses

400Failed to decrypt API key
401Invalid or expired API key
403Google OAuth not enabled
500Google OAuth not configured

State expires after 15 minutes.

GET /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 Google
stateEncrypted state (user_id + created_at)

Redirect URL Format

{success_redirect_uri}?auth_code={encrypted_auth_code}

How It Works

  1. Receives code and state from Google
  2. Decrypts & validates state (15-min expiry)
  3. Exchanges code for Google access token
  4. Gets user email from Google
  5. Creates or updates client with random password
  6. Generates auth_code (encrypted email + datetime)
  7. Redirects with auth_code in query string

⚠️ auth_code expires after 15 seconds β€” exchange it immediately.

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

X-API-Key: YOUR_API_TOKEN
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"
}
Exchange auth_code for JWT β€” JavaScript
// 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"
GET /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

Figma Plugin Quick Start β€” main.ts
// 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.